Commit 1e1e3ec6a30f784da596768d9cb44836e39155f1

Authored by Igor Kulikov
1 parent 5f8e5925

Introduce SMS Service. Add Send SMS Rule Node

Showing 47 changed files with 1888 additions and 41 deletions
@@ -194,6 +194,14 @@ @@ -194,6 +194,14 @@
194 <artifactId>javax.mail</artifactId> 194 <artifactId>javax.mail</artifactId>
195 </dependency> 195 </dependency>
196 <dependency> 196 <dependency>
  197 + <groupId>com.twilio.sdk</groupId>
  198 + <artifactId>twilio</artifactId>
  199 + </dependency>
  200 + <dependency>
  201 + <groupId>com.amazonaws</groupId>
  202 + <artifactId>aws-java-sdk-sns</artifactId>
  203 + </dependency>
  204 + <dependency>
197 <groupId>org.apache.curator</groupId> 205 <groupId>org.apache.curator</groupId>
198 <artifactId>curator-recipes</artifactId> 206 <artifactId>curator-recipes</artifactId>
199 </dependency> 207 </dependency>
@@ -32,6 +32,8 @@ import org.springframework.data.redis.core.RedisTemplate; @@ -32,6 +32,8 @@ import org.springframework.data.redis.core.RedisTemplate;
32 import org.springframework.scheduling.annotation.Scheduled; 32 import org.springframework.scheduling.annotation.Scheduled;
33 import org.springframework.stereotype.Component; 33 import org.springframework.stereotype.Component;
34 import org.thingsboard.rule.engine.api.MailService; 34 import org.thingsboard.rule.engine.api.MailService;
  35 +import org.thingsboard.rule.engine.api.SmsService;
  36 +import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
35 import org.thingsboard.server.actors.service.ActorService; 37 import org.thingsboard.server.actors.service.ActorService;
36 import org.thingsboard.server.actors.tenant.DebugTbRateLimits; 38 import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
37 import org.thingsboard.server.common.data.DataConstants; 39 import org.thingsboard.server.common.data.DataConstants;
@@ -80,6 +82,7 @@ import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService; @@ -80,6 +82,7 @@ import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
80 import org.thingsboard.server.service.script.JsExecutorService; 82 import org.thingsboard.server.service.script.JsExecutorService;
81 import org.thingsboard.server.service.script.JsInvokeService; 83 import org.thingsboard.server.service.script.JsInvokeService;
82 import org.thingsboard.server.service.session.DeviceSessionCacheService; 84 import org.thingsboard.server.service.session.DeviceSessionCacheService;
  85 +import org.thingsboard.server.service.sms.SmsExecutorService;
83 import org.thingsboard.server.service.state.DeviceStateService; 86 import org.thingsboard.server.service.state.DeviceStateService;
84 import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; 87 import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
85 import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; 88 import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
@@ -230,6 +233,10 @@ public class ActorSystemContext { @@ -230,6 +233,10 @@ public class ActorSystemContext {
230 233
231 @Autowired 234 @Autowired
232 @Getter 235 @Getter
  236 + private SmsExecutorService smsExecutor;
  237 +
  238 + @Autowired
  239 + @Getter
233 private DbCallbackExecutorService dbCallbackExecutor; 240 private DbCallbackExecutorService dbCallbackExecutor;
234 241
235 @Autowired 242 @Autowired
@@ -246,6 +253,14 @@ public class ActorSystemContext { @@ -246,6 +253,14 @@ public class ActorSystemContext {
246 253
247 @Autowired 254 @Autowired
248 @Getter 255 @Getter
  256 + private SmsService smsService;
  257 +
  258 + @Autowired
  259 + @Getter
  260 + private SmsSenderFactory smsSenderFactory;
  261 +
  262 + @Autowired
  263 + @Getter
249 private ClaimDevicesService claimDevicesService; 264 private ClaimDevicesService claimDevicesService;
250 265
251 @Autowired 266 @Autowired
@@ -325,6 +340,10 @@ public class ActorSystemContext { @@ -325,6 +340,10 @@ public class ActorSystemContext {
325 @Getter 340 @Getter
326 private boolean allowSystemMailService; 341 private boolean allowSystemMailService;
327 342
  343 + @Value("${actors.rule.allow_system_sms_service}")
  344 + @Getter
  345 + private boolean allowSystemSmsService;
  346 +
328 @Value("${transport.sessions.inactivity_timeout}") 347 @Value("${transport.sessions.inactivity_timeout}")
329 @Getter 348 @Getter
330 private long sessionInactivityTimeout; 349 private long sessionInactivityTimeout;
@@ -28,8 +28,10 @@ import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; @@ -28,8 +28,10 @@ import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
28 import org.thingsboard.rule.engine.api.RuleEngineRpcService; 28 import org.thingsboard.rule.engine.api.RuleEngineRpcService;
29 import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; 29 import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
30 import org.thingsboard.rule.engine.api.ScriptEngine; 30 import org.thingsboard.rule.engine.api.ScriptEngine;
  31 +import org.thingsboard.rule.engine.api.SmsService;
31 import org.thingsboard.rule.engine.api.TbContext; 32 import org.thingsboard.rule.engine.api.TbContext;
32 import org.thingsboard.rule.engine.api.TbRelationTypes; 33 import org.thingsboard.rule.engine.api.TbRelationTypes;
  34 +import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
33 import org.thingsboard.server.actors.ActorSystemContext; 35 import org.thingsboard.server.actors.ActorSystemContext;
34 import org.thingsboard.server.actors.TbActorRef; 36 import org.thingsboard.server.actors.TbActorRef;
35 import org.thingsboard.server.common.data.Customer; 37 import org.thingsboard.server.common.data.Customer;
@@ -303,6 +305,11 @@ class DefaultTbContext implements TbContext { @@ -303,6 +305,11 @@ class DefaultTbContext implements TbContext {
303 } 305 }
304 306
305 @Override 307 @Override
  308 + public ListeningExecutor getSmsExecutor() {
  309 + return mainCtx.getSmsExecutor();
  310 + }
  311 +
  312 + @Override
306 public ListeningExecutor getDbCallbackExecutor() { 313 public ListeningExecutor getDbCallbackExecutor() {
307 return mainCtx.getDbCallbackExecutor(); 314 return mainCtx.getDbCallbackExecutor();
308 } 315 }
@@ -428,6 +435,20 @@ class DefaultTbContext implements TbContext { @@ -428,6 +435,20 @@ class DefaultTbContext implements TbContext {
428 } 435 }
429 436
430 @Override 437 @Override
  438 + public SmsService getSmsService() {
  439 + if (mainCtx.isAllowSystemSmsService()) {
  440 + return mainCtx.getSmsService();
  441 + } else {
  442 + throw new RuntimeException("Access to System SMS Service is forbidden!");
  443 + }
  444 + }
  445 +
  446 + @Override
  447 + public SmsSenderFactory getSmsSenderFactory() {
  448 + return mainCtx.getSmsSenderFactory();
  449 + }
  450 +
  451 + @Override
431 public RuleEngineRpcService getRpcService() { 452 public RuleEngineRpcService getRpcService() {
432 return mainCtx.getTbRuleEngineDeviceRpcService(); 453 return mainCtx.getTbRuleEngineDeviceRpcService();
433 } 454 }
@@ -25,6 +25,8 @@ import org.springframework.web.bind.annotation.RequestMethod; @@ -25,6 +25,8 @@ import org.springframework.web.bind.annotation.RequestMethod;
25 import org.springframework.web.bind.annotation.ResponseBody; 25 import org.springframework.web.bind.annotation.ResponseBody;
26 import org.springframework.web.bind.annotation.RestController; 26 import org.springframework.web.bind.annotation.RestController;
27 import org.thingsboard.rule.engine.api.MailService; 27 import org.thingsboard.rule.engine.api.MailService;
  28 +import org.thingsboard.rule.engine.api.SmsService;
  29 +import org.thingsboard.rule.engine.api.sms.config.TestSmsRequest;
28 import org.thingsboard.server.common.data.AdminSettings; 30 import org.thingsboard.server.common.data.AdminSettings;
29 import org.thingsboard.server.common.data.UpdateMessage; 31 import org.thingsboard.server.common.data.UpdateMessage;
30 import org.thingsboard.server.common.data.exception.ThingsboardException; 32 import org.thingsboard.server.common.data.exception.ThingsboardException;
@@ -46,6 +48,9 @@ public class AdminController extends BaseController { @@ -46,6 +48,9 @@ public class AdminController extends BaseController {
46 private MailService mailService; 48 private MailService mailService;
47 49
48 @Autowired 50 @Autowired
  51 + private SmsService smsService;
  52 +
  53 + @Autowired
49 private AdminSettingsService adminSettingsService; 54 private AdminSettingsService adminSettingsService;
50 55
51 @Autowired 56 @Autowired
@@ -80,6 +85,8 @@ public class AdminController extends BaseController { @@ -80,6 +85,8 @@ public class AdminController extends BaseController {
80 if (adminSettings.getKey().equals("mail")) { 85 if (adminSettings.getKey().equals("mail")) {
81 mailService.updateMailConfiguration(); 86 mailService.updateMailConfiguration();
82 ((ObjectNode) adminSettings.getJsonValue()).put("password", ""); 87 ((ObjectNode) adminSettings.getJsonValue()).put("password", "");
  88 + } else if (adminSettings.getKey().equals("sms")) {
  89 + smsService.updateSmsConfiguration();
83 } 90 }
84 return adminSettings; 91 return adminSettings;
85 } catch (Exception e) { 92 } catch (Exception e) {
@@ -128,6 +135,17 @@ public class AdminController extends BaseController { @@ -128,6 +135,17 @@ public class AdminController extends BaseController {
128 } 135 }
129 136
130 @PreAuthorize("hasAuthority('SYS_ADMIN')") 137 @PreAuthorize("hasAuthority('SYS_ADMIN')")
  138 + @RequestMapping(value = "/settings/testSms", method = RequestMethod.POST)
  139 + public void sendTestSms(@RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException {
  140 + try {
  141 + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
  142 + smsService.sendTestSms(testSmsRequest);
  143 + } catch (Exception e) {
  144 + throw handleException(e);
  145 + }
  146 + }
  147 +
  148 + @PreAuthorize("hasAuthority('SYS_ADMIN')")
131 @RequestMapping(value = "/updates", method = RequestMethod.GET) 149 @RequestMapping(value = "/updates", method = RequestMethod.GET)
132 @ResponseBody 150 @ResponseBody
133 public UpdateMessage checkUpdates() throws ThingsboardException { 151 public UpdateMessage checkUpdates() throws ThingsboardException {
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.sms;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.thingsboard.rule.engine.api.sms.SmsSender;
  20 +import org.thingsboard.rule.engine.api.sms.exception.SmsParseException;
  21 +import sun.misc.Regexp;
  22 +
  23 +import java.util.regex.Pattern;
  24 +
  25 +@Slf4j
  26 +public abstract class AbstractSmsSender implements SmsSender {
  27 +
  28 + private static final Pattern E_164_PHONE_NUMBER_PATTERN = Pattern.compile("^\\+[1-9]\\d{1,14}$");
  29 +
  30 + private static final int MAX_SMS_MESSAGE_LENGTH = 1600;
  31 + private static final int MAX_SMS_SEGMENT_LENGTH = 70;
  32 +
  33 + protected String validatePhoneNumber(String phoneNumber) throws SmsParseException {
  34 + phoneNumber = phoneNumber.trim();
  35 + if (!E_164_PHONE_NUMBER_PATTERN.matcher(phoneNumber).matches()) {
  36 + throw new SmsParseException("Invalid phone number format. Phone number must be in E.164 format.");
  37 + }
  38 + return phoneNumber;
  39 + }
  40 +
  41 + protected String prepareMessage(String message) {
  42 + message = message.replaceAll("^\"|\"$", "").replaceAll("\\\\n", "\n");
  43 + if (message.length() > MAX_SMS_MESSAGE_LENGTH) {
  44 + log.warn("SMS message exceeds maximum symbols length and will be truncated");
  45 + message = message.substring(0, MAX_SMS_MESSAGE_LENGTH);
  46 + }
  47 + return message;
  48 + }
  49 +
  50 + protected int countMessageSegments(String message) {
  51 + return (int)Math.ceil((double) message.length() / (double) MAX_SMS_SEGMENT_LENGTH);
  52 + }
  53 +
  54 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.sms;
  17 +
  18 +import org.springframework.stereotype.Component;
  19 +import org.thingsboard.rule.engine.api.sms.SmsSender;
  20 +import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
  21 +import org.thingsboard.rule.engine.api.sms.config.AwsSnsSmsProviderConfiguration;
  22 +import org.thingsboard.rule.engine.api.sms.config.SmsProviderConfiguration;
  23 +import org.thingsboard.rule.engine.api.sms.config.TwilioSmsProviderConfiguration;
  24 +import org.thingsboard.server.service.sms.aws.AwsSmsSender;
  25 +import org.thingsboard.server.service.sms.twilio.TwilioSmsSender;
  26 +
  27 +@Component
  28 +public class DefaultSmsSenderFactory implements SmsSenderFactory {
  29 +
  30 + @Override
  31 + public SmsSender createSmsSender(SmsProviderConfiguration config) {
  32 + switch (config.getType()) {
  33 + case AWS_SNS:
  34 + return new AwsSmsSender((AwsSnsSmsProviderConfiguration)config);
  35 + case TWILIO:
  36 + return new TwilioSmsSender((TwilioSmsProviderConfiguration)config);
  37 + default:
  38 + throw new RuntimeException("Unknown SMS provider type " + config.getType());
  39 + }
  40 + }
  41 +
  42 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.sms;
  17 +
  18 +import com.fasterxml.jackson.databind.JsonNode;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.springframework.beans.factory.annotation.Autowired;
  21 +import org.springframework.core.NestedRuntimeException;
  22 +import org.springframework.stereotype.Service;
  23 +import org.thingsboard.rule.engine.api.SmsService;
  24 +import org.thingsboard.rule.engine.api.sms.SmsSender;
  25 +import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
  26 +import org.thingsboard.rule.engine.api.sms.config.SmsProviderConfiguration;
  27 +import org.thingsboard.rule.engine.api.sms.config.TestSmsRequest;
  28 +import org.thingsboard.server.common.data.AdminSettings;
  29 +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
  30 +import org.thingsboard.server.common.data.exception.ThingsboardException;
  31 +import org.thingsboard.server.common.data.id.EntityId;
  32 +import org.thingsboard.server.common.data.id.TenantId;
  33 +import org.thingsboard.server.dao.settings.AdminSettingsService;
  34 +import org.thingsboard.server.dao.util.mapping.JacksonUtil;
  35 +
  36 +import javax.annotation.PostConstruct;
  37 +import javax.annotation.PreDestroy;
  38 +
  39 +@Service
  40 +@Slf4j
  41 +public class DefaultSmsService implements SmsService {
  42 +
  43 + @Autowired
  44 + private SmsSenderFactory smsSenderFactory;
  45 +
  46 + @Autowired
  47 + private AdminSettingsService adminSettingsService;
  48 +
  49 + private SmsSender smsSender;
  50 +
  51 + @PostConstruct
  52 + private void init() {
  53 + updateSmsConfiguration();
  54 + }
  55 +
  56 + @PreDestroy
  57 + private void destroy() {
  58 + if (this.smsSender != null) {
  59 + this.smsSender.destroy();
  60 + }
  61 + }
  62 +
  63 + @Override
  64 + public void updateSmsConfiguration() {
  65 + AdminSettings settings = adminSettingsService.findAdminSettingsByKey(new TenantId(EntityId.NULL_UUID), "sms");
  66 + if (settings != null) {
  67 + try {
  68 + JsonNode jsonConfig = settings.getJsonValue();
  69 + SmsProviderConfiguration configuration = JacksonUtil.convertValue(jsonConfig, SmsProviderConfiguration.class);
  70 + SmsSender newSmsSender = this.smsSenderFactory.createSmsSender(configuration);
  71 + if (this.smsSender != null) {
  72 + this.smsSender.destroy();
  73 + }
  74 + this.smsSender = newSmsSender;
  75 + } catch (Exception e) {
  76 + log.error("Failed to create SMS sender", e);
  77 + }
  78 + }
  79 + }
  80 +
  81 + @Override
  82 + public void sendSms(String numberTo, String message) throws ThingsboardException {
  83 + if (this.smsSender == null) {
  84 + throw new ThingsboardException("Unable to send SMS: no SMS provider configured!", ThingsboardErrorCode.GENERAL);
  85 + }
  86 + this.sendSms(this.smsSender, numberTo, message);
  87 + }
  88 +
  89 + @Override
  90 + public void sendSms(String[] numbersTo, String message) throws ThingsboardException {
  91 + for (String numberTo : numbersTo) {
  92 + this.sendSms(numberTo, message);
  93 + }
  94 + }
  95 +
  96 + @Override
  97 + public void sendTestSms(TestSmsRequest testSmsRequest) throws ThingsboardException {
  98 + SmsSender testSmsSender;
  99 + try {
  100 + testSmsSender = this.smsSenderFactory.createSmsSender(testSmsRequest.getProviderConfiguration());
  101 + } catch (Exception e) {
  102 + throw handleException(e);
  103 + }
  104 + this.sendSms(testSmsSender, testSmsRequest.getNumberTo(), testSmsRequest.getMessage());
  105 + testSmsSender.destroy();
  106 + }
  107 +
  108 + private int sendSms(SmsSender smsSender, String numberTo, String message) throws ThingsboardException {
  109 + try {
  110 + return smsSender.sendSms(numberTo, message);
  111 + } catch (Exception e) {
  112 + throw handleException(e);
  113 + }
  114 + }
  115 +
  116 + private ThingsboardException handleException(Exception exception) {
  117 + String message;
  118 + if (exception instanceof NestedRuntimeException) {
  119 + message = ((NestedRuntimeException) exception).getMostSpecificCause().getMessage();
  120 + } else {
  121 + message = exception.getMessage();
  122 + }
  123 + log.warn("Unable to send SMS: {}", message);
  124 + return new ThingsboardException(String.format("Unable to send SMS: %s", message),
  125 + ThingsboardErrorCode.GENERAL);
  126 + }
  127 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.sms;
  17 +
  18 +import org.springframework.beans.factory.annotation.Value;
  19 +import org.springframework.stereotype.Component;
  20 +import org.thingsboard.common.util.AbstractListeningExecutor;
  21 +
  22 +@Component
  23 +public class SmsExecutorService extends AbstractListeningExecutor {
  24 +
  25 + @Value("${actors.rule.sms_thread_pool_size}")
  26 + private int smsExecutorThreadPoolSize;
  27 +
  28 + @Override
  29 + protected int getThreadPollSize() {
  30 + return smsExecutorThreadPoolSize;
  31 + }
  32 +
  33 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.sms.aws;
  17 +
  18 +import com.amazonaws.auth.AWSCredentials;
  19 +import com.amazonaws.auth.AWSStaticCredentialsProvider;
  20 +import com.amazonaws.auth.BasicAWSCredentials;
  21 +import com.amazonaws.services.sns.AmazonSNS;
  22 +import com.amazonaws.services.sns.AmazonSNSClient;
  23 +import com.amazonaws.services.sns.model.PublishRequest;
  24 +import lombok.extern.slf4j.Slf4j;
  25 +import org.apache.commons.lang3.StringUtils;
  26 +import org.thingsboard.rule.engine.api.sms.config.AwsSnsSmsProviderConfiguration;
  27 +import org.thingsboard.rule.engine.api.sms.exception.SmsException;
  28 +import org.thingsboard.rule.engine.api.sms.exception.SmsSendException;
  29 +import org.thingsboard.server.service.sms.AbstractSmsSender;
  30 +
  31 +@Slf4j
  32 +public class AwsSmsSender extends AbstractSmsSender {
  33 +
  34 + private AmazonSNS snsClient;
  35 +
  36 + public AwsSmsSender(AwsSnsSmsProviderConfiguration config) {
  37 + if (StringUtils.isEmpty(config.getAccessKeyId()) || StringUtils.isEmpty(config.getSecretAccessKey()) || StringUtils.isEmpty(config.getRegion())) {
  38 + throw new IllegalArgumentException("Invalid AWS sms provider configuration: aws accessKeyId, aws secretAccessKey and aws region should be specified!");
  39 + }
  40 + AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKeyId(), config.getSecretAccessKey());
  41 + AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
  42 + this.snsClient = AmazonSNSClient.builder()
  43 + .withCredentials(credProvider)
  44 + .withRegion(config.getRegion())
  45 + .build();
  46 + }
  47 +
  48 + @Override
  49 + public int sendSms(String numberTo, String message) throws SmsException {
  50 + numberTo = this.validatePhoneNumber(numberTo);
  51 + message = this.prepareMessage(message);
  52 + try {
  53 + PublishRequest publishRequest = new PublishRequest()
  54 + .withPhoneNumber(numberTo)
  55 + .withMessage(message);
  56 + this.snsClient.publish(publishRequest);
  57 + return this.countMessageSegments(message);
  58 + } catch (Exception e) {
  59 + throw new SmsSendException("Failed to send SMS message - " + e.getMessage(), e);
  60 + }
  61 + }
  62 +
  63 + @Override
  64 + public void destroy() {
  65 + if (this.snsClient != null) {
  66 + try {
  67 + this.snsClient.shutdown();
  68 + } catch (Exception e) {
  69 + log.error("Failed to shutdown SNS client during destroy()", e);
  70 + }
  71 + }
  72 + }
  73 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.sms.twilio;
  17 +
  18 +import com.twilio.http.TwilioRestClient;
  19 +import com.twilio.rest.api.v2010.account.Message;
  20 +import com.twilio.type.PhoneNumber;
  21 +import org.apache.commons.lang3.StringUtils;
  22 +import org.thingsboard.rule.engine.api.sms.config.TwilioSmsProviderConfiguration;
  23 +import org.thingsboard.rule.engine.api.sms.exception.SmsException;
  24 +import org.thingsboard.rule.engine.api.sms.exception.SmsSendException;
  25 +import org.thingsboard.server.service.sms.AbstractSmsSender;
  26 +
  27 +public class TwilioSmsSender extends AbstractSmsSender {
  28 +
  29 + private TwilioRestClient twilioRestClient;
  30 + private String numberFrom;
  31 +
  32 + public TwilioSmsSender(TwilioSmsProviderConfiguration config) {
  33 + if (StringUtils.isEmpty(config.getAccountSid()) || StringUtils.isEmpty(config.getAccountToken()) || StringUtils.isEmpty(config.getNumberFrom())) {
  34 + throw new IllegalArgumentException("Invalid twilio sms provider configuration: accountSid, accountToken and numberFrom should be specified!");
  35 + }
  36 + this.numberFrom = this.validatePhoneNumber(config.getNumberFrom());
  37 + this.twilioRestClient = new TwilioRestClient.Builder(config.getAccountSid(), config.getAccountToken()).build();
  38 + }
  39 +
  40 + @Override
  41 + public int sendSms(String numberTo, String message) throws SmsException {
  42 + numberTo = this.validatePhoneNumber(numberTo);
  43 + message = this.prepareMessage(message);
  44 + try {
  45 + String numSegments = Message.creator(new PhoneNumber(numberTo), new PhoneNumber(this.numberFrom), message).create(this.twilioRestClient).getNumSegments();
  46 + return Integer.valueOf(numSegments);
  47 + } catch (Exception e) {
  48 + throw new SmsSendException("Failed to send SMS message - " + e.getMessage(), e);
  49 + }
  50 + }
  51 +
  52 + @Override
  53 + public void destroy() {
  54 +
  55 + }
  56 +}
@@ -281,8 +281,12 @@ actors: @@ -281,8 +281,12 @@ actors:
281 js_thread_pool_size: "${ACTORS_RULE_JS_THREAD_POOL_SIZE:50}" 281 js_thread_pool_size: "${ACTORS_RULE_JS_THREAD_POOL_SIZE:50}"
282 # Specify thread pool size for mail sender executor service 282 # Specify thread pool size for mail sender executor service
283 mail_thread_pool_size: "${ACTORS_RULE_MAIL_THREAD_POOL_SIZE:50}" 283 mail_thread_pool_size: "${ACTORS_RULE_MAIL_THREAD_POOL_SIZE:50}"
  284 + # Specify thread pool size for sms sender executor service
  285 + sms_thread_pool_size: "${ACTORS_RULE_SMS_THREAD_POOL_SIZE:50}"
284 # Whether to allow usage of system mail service for rules 286 # Whether to allow usage of system mail service for rules
285 allow_system_mail_service: "${ACTORS_RULE_ALLOW_SYSTEM_MAIL_SERVICE:true}" 287 allow_system_mail_service: "${ACTORS_RULE_ALLOW_SYSTEM_MAIL_SERVICE:true}"
  288 + # Whether to allow usage of system sms service for rules
  289 + allow_system_sms_service: "${ACTORS_RULE_ALLOW_SYSTEM_SMS_SERVICE:true}"
286 # Specify thread pool size for external call service 290 # Specify thread pool size for external call service
287 external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:50}" 291 external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:50}"
288 chain: 292 chain:
@@ -97,7 +97,7 @@ @@ -97,7 +97,7 @@
97 <fst.version>2.57</fst.version> 97 <fst.version>2.57</fst.version>
98 <antlr.version>2.7.7</antlr.version> 98 <antlr.version>2.7.7</antlr.version>
99 <snakeyaml.version>1.27</snakeyaml.version> 99 <snakeyaml.version>1.27</snakeyaml.version>
100 - <amazonaws.sqs.version>1.11.747</amazonaws.sqs.version> 100 + <aws.sdk.version>1.11.747</aws.sdk.version>
101 <pubsub.client.version>1.105.0</pubsub.client.version> 101 <pubsub.client.version>1.105.0</pubsub.client.version>
102 <azure-servicebus.version>3.2.0</azure-servicebus.version> 102 <azure-servicebus.version>3.2.0</azure-servicebus.version>
103 <passay.version>1.5.0</passay.version> 103 <passay.version>1.5.0</passay.version>
@@ -108,6 +108,7 @@ @@ -108,6 +108,7 @@
108 <micrometer.version>1.5.2</micrometer.version> 108 <micrometer.version>1.5.2</micrometer.version>
109 <protobuf-dynamic.version>1.0.2TB</protobuf-dynamic.version> 109 <protobuf-dynamic.version>1.0.2TB</protobuf-dynamic.version>
110 <wire-schema.version>3.4.0</wire-schema.version> 110 <wire-schema.version>3.4.0</wire-schema.version>
  111 + <twilio.version>7.54.2</twilio.version>
111 </properties> 112 </properties>
112 113
113 <modules> 114 <modules>
@@ -1318,7 +1319,12 @@ @@ -1318,7 +1319,12 @@
1318 <dependency> 1319 <dependency>
1319 <groupId>com.amazonaws</groupId> 1320 <groupId>com.amazonaws</groupId>
1320 <artifactId>aws-java-sdk-sqs</artifactId> 1321 <artifactId>aws-java-sdk-sqs</artifactId>
1321 - <version>${amazonaws.sqs.version}</version> 1322 + <version>${aws.sdk.version}</version>
  1323 + </dependency>
  1324 + <dependency>
  1325 + <groupId>com.amazonaws</groupId>
  1326 + <artifactId>aws-java-sdk-sns</artifactId>
  1327 + <version>${aws.sdk.version}</version>
1322 </dependency> 1328 </dependency>
1323 <dependency> 1329 <dependency>
1324 <groupId>com.google.cloud</groupId> 1330 <groupId>com.google.cloud</groupId>
@@ -1381,6 +1387,25 @@ @@ -1381,6 +1387,25 @@
1381 <artifactId>wire-schema</artifactId> 1387 <artifactId>wire-schema</artifactId>
1382 <version>${wire-schema.version}</version> 1388 <version>${wire-schema.version}</version>
1383 </dependency> 1389 </dependency>
  1390 + <dependency>
  1391 + <groupId>com.twilio.sdk</groupId>
  1392 + <artifactId>twilio</artifactId>
  1393 + <version>${twilio.version}</version>
  1394 + <exclusions>
  1395 + <exclusion>
  1396 + <groupId>io.jsonwebtoken</groupId>
  1397 + <artifactId>jjwt-api</artifactId>
  1398 + </exclusion>
  1399 + <exclusion>
  1400 + <groupId>io.jsonwebtoken</groupId>
  1401 + <artifactId>jjwt-jackson</artifactId>
  1402 + </exclusion>
  1403 + <exclusion>
  1404 + <groupId>io.jsonwebtoken</groupId>
  1405 + <artifactId>jjwt-impl</artifactId>
  1406 + </exclusion>
  1407 + </exclusions>
  1408 + </dependency>
1384 </dependencies> 1409 </dependencies>
1385 </dependencyManagement> 1410 </dependencyManagement>
1386 1411
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.api;
  17 +
  18 +import org.thingsboard.rule.engine.api.sms.config.TestSmsRequest;
  19 +import org.thingsboard.server.common.data.exception.ThingsboardException;
  20 +
  21 +public interface SmsService {
  22 +
  23 + void updateSmsConfiguration();
  24 +
  25 + void sendSms(String numberTo, String message) throws ThingsboardException;
  26 +
  27 + void sendSms(String[] numbersTo, String message) throws ThingsboardException;;
  28 +
  29 + void sendTestSms(TestSmsRequest testSmsRequest) throws ThingsboardException;
  30 +
  31 +}
@@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.api; @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.api;
18 import io.netty.channel.EventLoopGroup; 18 import io.netty.channel.EventLoopGroup;
19 import org.springframework.data.redis.core.RedisTemplate; 19 import org.springframework.data.redis.core.RedisTemplate;
20 import org.thingsboard.common.util.ListeningExecutor; 20 import org.thingsboard.common.util.ListeningExecutor;
  21 +import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
21 import org.thingsboard.server.common.data.Customer; 22 import org.thingsboard.server.common.data.Customer;
22 import org.thingsboard.server.common.data.Device; 23 import org.thingsboard.server.common.data.Device;
23 import org.thingsboard.server.common.data.DeviceProfile; 24 import org.thingsboard.server.common.data.DeviceProfile;
@@ -194,12 +195,18 @@ public interface TbContext { @@ -194,12 +195,18 @@ public interface TbContext {
194 195
195 ListeningExecutor getMailExecutor(); 196 ListeningExecutor getMailExecutor();
196 197
  198 + ListeningExecutor getSmsExecutor();
  199 +
197 ListeningExecutor getDbCallbackExecutor(); 200 ListeningExecutor getDbCallbackExecutor();
198 201
199 ListeningExecutor getExternalCallExecutor(); 202 ListeningExecutor getExternalCallExecutor();
200 203
201 MailService getMailService(); 204 MailService getMailService();
202 205
  206 + SmsService getSmsService();
  207 +
  208 + SmsSenderFactory getSmsSenderFactory();
  209 +
203 ScriptEngine createJsScriptEngine(String script, String... argNames); 210 ScriptEngine createJsScriptEngine(String script, String... argNames);
204 211
205 void logJsEvalRequest(); 212 void logJsEvalRequest();
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.api.sms;
  17 +
  18 +import org.thingsboard.rule.engine.api.sms.exception.SmsException;
  19 +
  20 +public interface SmsSender {
  21 +
  22 + int sendSms(String numberTo, String message) throws SmsException;
  23 +
  24 + void destroy();
  25 +
  26 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.api.sms;
  17 +
  18 +import org.thingsboard.rule.engine.api.sms.config.SmsProviderConfiguration;
  19 +
  20 +public interface SmsSenderFactory {
  21 +
  22 + SmsSender createSmsSender(SmsProviderConfiguration config);
  23 +
  24 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.api.sms.config;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class AwsSnsSmsProviderConfiguration implements SmsProviderConfiguration {
  22 +
  23 + private String accessKeyId;
  24 + private String secretAccessKey;
  25 + private String region;
  26 +
  27 + @Override
  28 + public SmsProviderType getType() {
  29 + return SmsProviderType.AWS_SNS;
  30 + }
  31 +
  32 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.api.sms.config;
  17 +
  18 +import com.fasterxml.jackson.annotation.JsonIgnore;
  19 +import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
  20 +import com.fasterxml.jackson.annotation.JsonSubTypes;
  21 +import com.fasterxml.jackson.annotation.JsonTypeInfo;
  22 +
  23 +@JsonIgnoreProperties(ignoreUnknown = true)
  24 +@JsonTypeInfo(
  25 + use = JsonTypeInfo.Id.NAME,
  26 + include = JsonTypeInfo.As.PROPERTY,
  27 + property = "type")
  28 +@JsonSubTypes({
  29 + @JsonSubTypes.Type(value = AwsSnsSmsProviderConfiguration.class, name = "AWS_SNS"),
  30 + @JsonSubTypes.Type(value = TwilioSmsProviderConfiguration.class, name = "TWILIO")})
  31 +public interface SmsProviderConfiguration {
  32 +
  33 + @JsonIgnore
  34 + SmsProviderType getType();
  35 +
  36 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.api.sms.config;
  17 +
  18 +public enum SmsProviderType {
  19 + AWS_SNS,
  20 + TWILIO
  21 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.api.sms.config;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class TestSmsRequest {
  22 +
  23 + private SmsProviderConfiguration providerConfiguration;
  24 + private String numberTo;
  25 + private String message;
  26 +
  27 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.api.sms.config;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class TwilioSmsProviderConfiguration implements SmsProviderConfiguration {
  22 +
  23 + private String accountSid;
  24 + private String accountToken;
  25 + private String numberFrom;
  26 +
  27 + @Override
  28 + public SmsProviderType getType() {
  29 + return SmsProviderType.TWILIO;
  30 + }
  31 +
  32 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.api.sms.exception;
  17 +
  18 +public abstract class SmsException extends RuntimeException {
  19 +
  20 + public SmsException(String msg) {
  21 + super(msg);
  22 + }
  23 +
  24 + public SmsException(String msg, Throwable cause) {
  25 + super(msg, cause);
  26 + }
  27 +
  28 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.api.sms.exception;
  17 +
  18 +public class SmsParseException extends SmsException {
  19 +
  20 + public SmsParseException(String msg) {
  21 + super(msg);
  22 + }
  23 +
  24 + public SmsParseException(String msg, Throwable cause) {
  25 + super(msg, cause);
  26 + }
  27 +
  28 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.api.sms.exception;
  17 +
  18 +public class SmsSendException extends SmsException {
  19 +
  20 + public SmsSendException(String msg) {
  21 + super(msg);
  22 + }
  23 +
  24 + public SmsSendException(String msg, Throwable cause) {
  25 + super(msg, cause);
  26 + }
  27 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.sms;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.thingsboard.rule.engine.api.RuleNode;
  20 +import org.thingsboard.rule.engine.api.TbContext;
  21 +import org.thingsboard.rule.engine.api.TbNode;
  22 +import org.thingsboard.rule.engine.api.TbNodeConfiguration;
  23 +import org.thingsboard.rule.engine.api.TbNodeException;
  24 +import org.thingsboard.rule.engine.api.sms.SmsSender;
  25 +import org.thingsboard.rule.engine.api.util.TbNodeUtils;
  26 +import org.thingsboard.server.common.data.plugin.ComponentType;
  27 +import org.thingsboard.server.common.msg.TbMsg;
  28 +
  29 +import static org.thingsboard.common.util.DonAsynchron.withCallback;
  30 +
  31 +@Slf4j
  32 +@RuleNode(
  33 + type = ComponentType.EXTERNAL,
  34 + name = "send sms",
  35 + configClazz = TbSendSmsNodeConfiguration.class,
  36 + nodeDescription = "Sends SMS message via SMS provider.",
  37 + nodeDetails = "Will send SMS message by populating target phone numbers and sms message fields using values derived from message metadata.",
  38 + uiResources = {"static/rulenode/rulenode-core-config.js"},
  39 + configDirective = "tbActionNodeSendSmsConfig",
  40 + icon = "sms"
  41 +)
  42 +public class TbSendSmsNode implements TbNode {
  43 +
  44 + private TbSendSmsNodeConfiguration config;
  45 + private SmsSender smsSender;
  46 +
  47 + @Override
  48 + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
  49 + try {
  50 + this.config = TbNodeUtils.convert(configuration, TbSendSmsNodeConfiguration.class);
  51 + if (!this.config.isUseSystemSmsSettings()) {
  52 + smsSender = createSmsSender(ctx);
  53 + }
  54 + } catch (Exception e) {
  55 + throw new TbNodeException(e);
  56 + }
  57 + }
  58 +
  59 + @Override
  60 + public void onMsg(TbContext ctx, TbMsg msg) {
  61 + try {
  62 + withCallback(ctx.getSmsExecutor().executeAsync(() -> {
  63 + sendSms(ctx, msg);
  64 + return null;
  65 + }),
  66 + ok -> ctx.tellSuccess(msg),
  67 + fail -> ctx.tellFailure(msg, fail));
  68 + } catch (Exception ex) {
  69 + ctx.tellFailure(msg, ex);
  70 + }
  71 + }
  72 +
  73 + private void sendSms(TbContext ctx, TbMsg msg) throws Exception {
  74 + String numbersTo = TbNodeUtils.processPattern(this.config.getNumbersToTemplate(), msg.getMetaData());
  75 + String message = TbNodeUtils.processPattern(this.config.getSmsMessageTemplate(), msg.getMetaData());
  76 + String[] numbersToList = numbersTo.split(",");
  77 + if (this.config.isUseSystemSmsSettings()) {
  78 + ctx.getSmsService().sendSms(numbersToList, message);
  79 + } else {
  80 + for (String numberTo : numbersToList) {
  81 + this.smsSender.sendSms(numberTo, message);
  82 + }
  83 + }
  84 + }
  85 +
  86 + @Override
  87 + public void destroy() {
  88 + if (this.smsSender != null) {
  89 + this.smsSender.destroy();
  90 + }
  91 + }
  92 +
  93 + private SmsSender createSmsSender(TbContext ctx) {
  94 + return ctx.getSmsSenderFactory().createSmsSender(this.config.getSmsProviderConfiguration());
  95 + }
  96 +
  97 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.sms;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
  20 +import org.thingsboard.rule.engine.api.sms.config.SmsProviderConfiguration;
  21 +
  22 +@Data
  23 +public class TbSendSmsNodeConfiguration implements NodeConfiguration {
  24 +
  25 + private String numbersToTemplate;
  26 + private String smsMessageTemplate;
  27 + private boolean useSystemSmsSettings;
  28 + private SmsProviderConfiguration smsProviderConfiguration;
  29 +
  30 + @Override
  31 + public NodeConfiguration defaultConfiguration() {
  32 + TbSendSmsNodeConfiguration configuration = new TbSendSmsNodeConfiguration();
  33 + configuration.numbersToTemplate = "${userPhone}";
  34 + configuration.smsMessageTemplate = "Device ${deviceName} has high temperature ${temp}";
  35 + configuration.setUseSystemSmsSettings(true);
  36 + return configuration;
  37 + }
  38 +}
@@ -18,7 +18,13 @@ import { Injectable } from '@angular/core'; @@ -18,7 +18,13 @@ import { Injectable } from '@angular/core';
18 import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; 18 import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
19 import { Observable } from 'rxjs'; 19 import { Observable } from 'rxjs';
20 import { HttpClient } from '@angular/common/http'; 20 import { HttpClient } from '@angular/common/http';
21 -import { AdminSettings, MailServerSettings, SecuritySettings, UpdateMessage } from '@shared/models/settings.models'; 21 +import {
  22 + AdminSettings,
  23 + MailServerSettings,
  24 + SecuritySettings,
  25 + TestSmsRequest,
  26 + UpdateMessage
  27 +} from '@shared/models/settings.models';
22 28
23 @Injectable({ 29 @Injectable({
24 providedIn: 'root' 30 providedIn: 'root'
@@ -43,6 +49,11 @@ export class AdminService { @@ -43,6 +49,11 @@ export class AdminService {
43 return this.http.post<void>('/api/admin/settings/testMail', adminSettings, defaultHttpOptionsFromConfig(config)); 49 return this.http.post<void>('/api/admin/settings/testMail', adminSettings, defaultHttpOptionsFromConfig(config));
44 } 50 }
45 51
  52 + public sendTestSms(testSmsRequest: TestSmsRequest,
  53 + config?: RequestConfig): Observable<void> {
  54 + return this.http.post<void>('/api/admin/settings/testSms', testSmsRequest, defaultHttpOptionsFromConfig(config));
  55 + }
  56 +
46 public getSecuritySettings(config?: RequestConfig): Observable<SecuritySettings> { 57 public getSecuritySettings(config?: RequestConfig): Observable<SecuritySettings> {
47 return this.http.get<SecuritySettings>(`/api/admin/securitySettings`, defaultHttpOptionsFromConfig(config)); 58 return this.http.get<SecuritySettings>(`/api/admin/securitySettings`, defaultHttpOptionsFromConfig(config));
48 } 59 }
@@ -108,7 +108,7 @@ export class MenuService { @@ -108,7 +108,7 @@ export class MenuService {
108 name: 'admin.system-settings', 108 name: 'admin.system-settings',
109 type: 'toggle', 109 type: 'toggle',
110 path: '/settings', 110 path: '/settings',
111 - height: '160px', 111 + height: '200px',
112 icon: 'settings', 112 icon: 'settings',
113 pages: [ 113 pages: [
114 { 114 {
@@ -127,6 +127,13 @@ export class MenuService { @@ -127,6 +127,13 @@ export class MenuService {
127 }, 127 },
128 { 128 {
129 id: guid(), 129 id: guid(),
  130 + name: 'admin.sms-provider',
  131 + type: 'link',
  132 + path: '/settings/sms-provider',
  133 + icon: 'sms'
  134 + },
  135 + {
  136 + id: guid(),
130 name: 'admin.security-settings', 137 name: 'admin.security-settings',
131 type: 'link', 138 type: 'link',
132 path: '/settings/security-settings', 139 path: '/settings/security-settings',
@@ -188,6 +195,11 @@ export class MenuService { @@ -188,6 +195,11 @@ export class MenuService {
188 path: '/settings/outgoing-mail' 195 path: '/settings/outgoing-mail'
189 }, 196 },
190 { 197 {
  198 + name: 'admin.sms-provider',
  199 + icon: 'sms',
  200 + path: '/settings/sms-provider'
  201 + },
  202 + {
191 name: 'admin.security-settings', 203 name: 'admin.security-settings',
192 icon: 'security', 204 icon: 'security',
193 path: '/settings/security-settings' 205 path: '/settings/security-settings'
@@ -77,44 +77,47 @@ import { ComplexFilterPredicateDialogComponent } from '@home/components/filter/c @@ -77,44 +77,47 @@ import { ComplexFilterPredicateDialogComponent } from '@home/components/filter/c
77 import { KeyFilterDialogComponent } from '@home/components/filter/key-filter-dialog.component'; 77 import { KeyFilterDialogComponent } from '@home/components/filter/key-filter-dialog.component';
78 import { FiltersDialogComponent } from '@home/components/filter/filters-dialog.component'; 78 import { FiltersDialogComponent } from '@home/components/filter/filters-dialog.component';
79 import { FilterDialogComponent } from '@home/components/filter/filter-dialog.component'; 79 import { FilterDialogComponent } from '@home/components/filter/filter-dialog.component';
80 -import { FilterSelectComponent } from './filter/filter-select.component'; 80 +import { FilterSelectComponent } from '@home/components/filter/filter-select.component';
81 import { FiltersEditComponent } from '@home/components/filter/filters-edit.component'; 81 import { FiltersEditComponent } from '@home/components/filter/filters-edit.component';
82 import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-panel.component'; 82 import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-panel.component';
83 import { UserFilterDialogComponent } from '@home/components/filter/user-filter-dialog.component'; 83 import { UserFilterDialogComponent } from '@home/components/filter/user-filter-dialog.component';
84 -import { FilterUserInfoComponent } from './filter/filter-user-info.component';  
85 -import { FilterUserInfoDialogComponent } from './filter/filter-user-info-dialog.component';  
86 -import { FilterPredicateValueComponent } from './filter/filter-predicate-value.component';  
87 -import { TenantProfileAutocompleteComponent } from './profile/tenant-profile-autocomplete.component';  
88 -import { TenantProfileComponent } from './profile/tenant-profile.component';  
89 -import { TenantProfileDialogComponent } from './profile/tenant-profile-dialog.component';  
90 -import { TenantProfileDataComponent } from './profile/tenant-profile-data.component';  
91 -import { DefaultDeviceProfileConfigurationComponent } from './profile/device/default-device-profile-configuration.component';  
92 -import { DeviceProfileConfigurationComponent } from './profile/device/device-profile-configuration.component';  
93 -import { DeviceProfileComponent } from './profile/device-profile.component';  
94 -import { DefaultDeviceProfileTransportConfigurationComponent } from './profile/device/default-device-profile-transport-configuration.component';  
95 -import { DeviceProfileTransportConfigurationComponent } from './profile/device/device-profile-transport-configuration.component';  
96 -import { DeviceProfileDialogComponent } from './profile/device-profile-dialog.component';  
97 -import { DeviceProfileAutocompleteComponent } from './profile/device-profile-autocomplete.component';  
98 -import { MqttDeviceProfileTransportConfigurationComponent } from './profile/device/mqtt-device-profile-transport-configuration.component';  
99 -import { Lwm2mDeviceProfileTransportConfigurationComponent } from './profile/device/lwm2m-device-profile-transport-configuration.component';  
100 -import { DeviceProfileAlarmsComponent } from './profile/alarm/device-profile-alarms.component';  
101 -import { DeviceProfileAlarmComponent } from './profile/alarm/device-profile-alarm.component';  
102 -import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component';  
103 -import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component';  
104 -import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component';  
105 -import { FilterTextComponent } from './filter/filter-text.component';  
106 -import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component';  
107 -import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component';  
108 -import { DeviceProfileProvisionConfigurationComponent } from "./profile/device-profile-provision-configuration.component";  
109 -import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component';  
110 -import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component';  
111 -import { DeviceCredentialsComponent } from './device/device-credentials.component';  
112 -import { AlarmScheduleInfoComponent } from './profile/alarm/alarm-schedule-info.component'; 84 +import { FilterUserInfoComponent } from '@home/components/filter/filter-user-info.component';
  85 +import { FilterUserInfoDialogComponent } from '@home/components/filter/filter-user-info-dialog.component';
  86 +import { FilterPredicateValueComponent } from '@home/components/filter/filter-predicate-value.component';
  87 +import { TenantProfileAutocompleteComponent } from '@home/components/profile/tenant-profile-autocomplete.component';
  88 +import { TenantProfileComponent } from '@home/components/profile/tenant-profile.component';
  89 +import { TenantProfileDialogComponent } from '@home/components/profile/tenant-profile-dialog.component';
  90 +import { TenantProfileDataComponent } from '@home/components/profile/tenant-profile-data.component';
  91 +import { DefaultDeviceProfileConfigurationComponent } from '@home/components/profile/device/default-device-profile-configuration.component';
  92 +import { DeviceProfileConfigurationComponent } from '@home/components/profile/device/device-profile-configuration.component';
  93 +import { DeviceProfileComponent } from '@home/components/profile/device-profile.component';
  94 +import { DefaultDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/default-device-profile-transport-configuration.component';
  95 +import { DeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/device-profile-transport-configuration.component';
  96 +import { DeviceProfileDialogComponent } from '@home/components/profile/device-profile-dialog.component';
  97 +import { DeviceProfileAutocompleteComponent } from '@home/components/profile/device-profile-autocomplete.component';
  98 +import { MqttDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/mqtt-device-profile-transport-configuration.component';
  99 +import { Lwm2mDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/lwm2m-device-profile-transport-configuration.component';
  100 +import { DeviceProfileAlarmsComponent } from '@home/components/profile/alarm/device-profile-alarms.component';
  101 +import { DeviceProfileAlarmComponent } from '@home/components/profile/alarm/device-profile-alarm.component';
  102 +import { CreateAlarmRulesComponent } from '@home/components/profile/alarm/create-alarm-rules.component';
  103 +import { AlarmRuleComponent } from '@home/components/profile/alarm/alarm-rule.component';
  104 +import { AlarmRuleConditionComponent } from '@home/components/profile/alarm/alarm-rule-condition.component';
  105 +import { FilterTextComponent } from '@home/components/filter/filter-text.component';
  106 +import { AddDeviceProfileDialogComponent } from '@home/components/profile/add-device-profile-dialog.component';
  107 +import { RuleChainAutocompleteComponent } from '@home/components/rule-chain/rule-chain-autocomplete.component';
  108 +import { DeviceProfileProvisionConfigurationComponent } from '@home/components/profile/device-profile-provision-configuration.component';
  109 +import { AlarmScheduleComponent } from '@home/components/profile/alarm/alarm-schedule.component';
  110 +import { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component';
  111 +import { DeviceCredentialsComponent } from '@home/components/device/device-credentials.component';
  112 +import { AlarmScheduleInfoComponent } from '@home/components/profile/alarm/alarm-schedule-info.component';
113 import { AlarmScheduleDialogComponent } from '@home/components/profile/alarm/alarm-schedule-dialog.component'; 113 import { AlarmScheduleDialogComponent } from '@home/components/profile/alarm/alarm-schedule-dialog.component';
114 -import { EditAlarmDetailsDialogComponent } from './profile/alarm/edit-alarm-details-dialog.component'; 114 +import { EditAlarmDetailsDialogComponent } from '@home/components/profile/alarm/edit-alarm-details-dialog.component';
115 import { AlarmRuleConditionDialogComponent } from '@home/components/profile/alarm/alarm-rule-condition-dialog.component'; 115 import { AlarmRuleConditionDialogComponent } from '@home/components/profile/alarm/alarm-rule-condition-dialog.component';
116 -import { DefaultTenantProfileConfigurationComponent } from './profile/tenant/default-tenant-profile-configuration.component';  
117 -import { TenantProfileConfigurationComponent } from './profile/tenant/tenant-profile-configuration.component'; 116 +import { DefaultTenantProfileConfigurationComponent } from '@home/components/profile/tenant/default-tenant-profile-configuration.component';
  117 +import { TenantProfileConfigurationComponent } from '@home/components/profile/tenant/tenant-profile-configuration.component';
  118 +import { SmsProviderConfigurationComponent } from '@home/components/sms/sms-provider-configuration.component';
  119 +import { AwsSnsProviderConfigurationComponent } from '@home/components/sms/aws-sns-provider-configuration.component';
  120 +import { TwilioSmsProviderConfigurationComponent } from '@home/components/sms/twilio-sms-provider-configuration.component';
118 121
119 @NgModule({ 122 @NgModule({
120 declarations: 123 declarations:
@@ -212,7 +215,10 @@ import { TenantProfileConfigurationComponent } from './profile/tenant/tenant-pro @@ -212,7 +215,10 @@ import { TenantProfileConfigurationComponent } from './profile/tenant/tenant-pro
212 DeviceWizardDialogComponent, 215 DeviceWizardDialogComponent,
213 DeviceCredentialsComponent, 216 DeviceCredentialsComponent,
214 AlarmScheduleDialogComponent, 217 AlarmScheduleDialogComponent,
215 - EditAlarmDetailsDialogComponent 218 + EditAlarmDetailsDialogComponent,
  219 + SmsProviderConfigurationComponent,
  220 + AwsSnsProviderConfigurationComponent,
  221 + TwilioSmsProviderConfigurationComponent
216 ], 222 ],
217 imports: [ 223 imports: [
218 CommonModule, 224 CommonModule,
@@ -298,7 +304,9 @@ import { TenantProfileConfigurationComponent } from './profile/tenant/tenant-pro @@ -298,7 +304,9 @@ import { TenantProfileConfigurationComponent } from './profile/tenant/tenant-pro
298 AlarmScheduleDialogComponent, 304 AlarmScheduleDialogComponent,
299 EditAlarmDetailsDialogComponent, 305 EditAlarmDetailsDialogComponent,
300 DeviceProfileProvisionConfigurationComponent, 306 DeviceProfileProvisionConfigurationComponent,
301 - AlarmScheduleComponent 307 + SmsProviderConfigurationComponent,
  308 + AwsSnsProviderConfigurationComponent,
  309 + TwilioSmsProviderConfigurationComponent
302 ], 310 ],
303 providers: [ 311 providers: [
304 WidgetComponentService, 312 WidgetComponentService,
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form [formGroup]="awsSnsProviderConfigurationFormGroup" style="padding-bottom: 16px;">
  19 + <mat-form-field class="mat-block">
  20 + <mat-label translate>admin.aws-access-key-id</mat-label>
  21 + <input required matInput formControlName="accessKeyId">
  22 + <mat-error *ngIf="awsSnsProviderConfigurationFormGroup.get('accessKeyId').hasError('required')">
  23 + {{ 'admin.aws-access-key-id-required' | translate }}
  24 + </mat-error>
  25 + </mat-form-field>
  26 + <mat-form-field class="mat-block">
  27 + <mat-label translate>admin.aws-secret-access-key</mat-label>
  28 + <input required type="password" matInput formControlName="secretAccessKey">
  29 + <mat-error *ngIf="awsSnsProviderConfigurationFormGroup.get('secretAccessKey').hasError('required')">
  30 + {{ 'admin.aws-secret-access-key-required' | translate }}
  31 + </mat-error>
  32 + </mat-form-field>
  33 + <mat-form-field class="mat-block">
  34 + <mat-label translate>admin.aws-region</mat-label>
  35 + <input required matInput formControlName="region">
  36 + <mat-error *ngIf="awsSnsProviderConfigurationFormGroup.get('region').hasError('required')">
  37 + {{ 'admin.aws-region-required' | translate }}
  38 + </mat-error>
  39 + </mat-form-field>
  40 +</form>
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, forwardRef, Input, OnInit } from '@angular/core';
  18 +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@app/core/core.state';
  21 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  22 +import { isDefinedAndNotNull } from '@core/utils';
  23 +import { AwsSnsSmsProviderConfiguration } from '@shared/models/settings.models';
  24 +
  25 +@Component({
  26 + selector: 'tb-aws-sns-provider-configuration',
  27 + templateUrl: './aws-sns-provider-configuration.component.html',
  28 + styleUrls: [],
  29 + providers: [{
  30 + provide: NG_VALUE_ACCESSOR,
  31 + useExisting: forwardRef(() => AwsSnsProviderConfigurationComponent),
  32 + multi: true
  33 + }]
  34 +})
  35 +export class AwsSnsProviderConfigurationComponent implements ControlValueAccessor, OnInit {
  36 +
  37 + awsSnsProviderConfigurationFormGroup: FormGroup;
  38 +
  39 + private requiredValue: boolean;
  40 +
  41 + get required(): boolean {
  42 + return this.requiredValue;
  43 + }
  44 +
  45 + @Input()
  46 + set required(value: boolean) {
  47 + this.requiredValue = coerceBooleanProperty(value);
  48 + }
  49 +
  50 + @Input()
  51 + disabled: boolean;
  52 +
  53 + private propagateChange = (v: any) => { };
  54 +
  55 + constructor(private store: Store<AppState>,
  56 + private fb: FormBuilder) {
  57 + }
  58 +
  59 + registerOnChange(fn: any): void {
  60 + this.propagateChange = fn;
  61 + }
  62 +
  63 + registerOnTouched(fn: any): void {
  64 + }
  65 +
  66 + ngOnInit() {
  67 + this.awsSnsProviderConfigurationFormGroup = this.fb.group({
  68 + accessKeyId: [null, [Validators.required]],
  69 + secretAccessKey: [null, [Validators.required]],
  70 + region: [null, [Validators.required]]
  71 + });
  72 + this.awsSnsProviderConfigurationFormGroup.valueChanges.subscribe(() => {
  73 + this.updateModel();
  74 + });
  75 + }
  76 +
  77 + setDisabledState(isDisabled: boolean): void {
  78 + this.disabled = isDisabled;
  79 + if (this.disabled) {
  80 + this.awsSnsProviderConfigurationFormGroup.disable({emitEvent: false});
  81 + } else {
  82 + this.awsSnsProviderConfigurationFormGroup.enable({emitEvent: false});
  83 + }
  84 + }
  85 +
  86 + writeValue(value: AwsSnsSmsProviderConfiguration | null): void {
  87 + if (isDefinedAndNotNull(value)) {
  88 + this.awsSnsProviderConfigurationFormGroup.patchValue(value, {emitEvent: false});
  89 + }
  90 + }
  91 +
  92 + private updateModel() {
  93 + let configuration: AwsSnsSmsProviderConfiguration = null;
  94 + if (this.awsSnsProviderConfigurationFormGroup.valid) {
  95 + configuration = this.awsSnsProviderConfigurationFormGroup.value;
  96 + }
  97 + this.propagateChange(configuration);
  98 + }
  99 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div [formGroup]="smsProviderConfigurationFormGroup">
  19 + <mat-form-field class="mat-block">
  20 + <mat-label translate>admin.sms-provider-type</mat-label>
  21 + <mat-select formControlName="type" [required]="required">
  22 + <mat-option *ngFor="let type of smsProviderTypes" [value]="type">
  23 + {{smsProviderTypeTranslations.get(type) | translate}}
  24 + </mat-option>
  25 + </mat-select>
  26 + <mat-error *ngIf="smsProviderConfigurationFormGroup.get('type').hasError('required')">
  27 + {{ 'admin.sms-provider-type-required' | translate }}
  28 + </mat-error>
  29 + </mat-form-field>
  30 + <div [ngSwitch]="smsProviderConfigurationFormGroup.get('type').value">
  31 + <ng-template [ngSwitchCase]="smsProviderType.AWS_SNS">
  32 + <tb-aws-sns-provider-configuration
  33 + [required]="required"
  34 + formControlName="configuration">
  35 + </tb-aws-sns-provider-configuration>
  36 + </ng-template>
  37 + <ng-template [ngSwitchCase]="smsProviderType.TWILIO">
  38 + <tb-twilio-sms-provider-configuration
  39 + [required]="required"
  40 + formControlName="configuration">
  41 + </tb-twilio-sms-provider-configuration>
  42 + </ng-template>
  43 + </div>
  44 +</div>
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, forwardRef, Input, OnInit } from '@angular/core';
  18 +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@app/core/core.state';
  21 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  22 +import {
  23 + DeviceProfileTransportConfiguration,
  24 + DeviceTransportType,
  25 + deviceTransportTypeTranslationMap
  26 +} from '@shared/models/device.models';
  27 +import { deepClone } from '@core/utils';
  28 +import {
  29 + createSmsProviderConfiguration,
  30 + SmsProviderConfiguration,
  31 + SmsProviderType,
  32 + smsProviderTypeTranslationMap
  33 +} from '@shared/models/settings.models';
  34 +
  35 +@Component({
  36 + selector: 'tb-sms-provider-configuration',
  37 + templateUrl: './sms-provider-configuration.component.html',
  38 + styleUrls: [],
  39 + providers: [{
  40 + provide: NG_VALUE_ACCESSOR,
  41 + useExisting: forwardRef(() => SmsProviderConfigurationComponent),
  42 + multi: true
  43 + }]
  44 +})
  45 +export class SmsProviderConfigurationComponent implements ControlValueAccessor, OnInit {
  46 +
  47 + smsProviderType = SmsProviderType;
  48 + smsProviderTypes = Object.keys(SmsProviderType);
  49 + smsProviderTypeTranslations = smsProviderTypeTranslationMap;
  50 +
  51 + smsProviderConfigurationFormGroup: FormGroup;
  52 +
  53 + private requiredValue: boolean;
  54 + get required(): boolean {
  55 + return this.requiredValue;
  56 + }
  57 + @Input()
  58 + set required(value: boolean) {
  59 + this.requiredValue = coerceBooleanProperty(value);
  60 + }
  61 +
  62 + @Input()
  63 + disabled: boolean;
  64 +
  65 + private propagateChange = (v: any) => { };
  66 +
  67 + constructor(private store: Store<AppState>,
  68 + private fb: FormBuilder) {
  69 + }
  70 +
  71 + registerOnChange(fn: any): void {
  72 + this.propagateChange = fn;
  73 + }
  74 +
  75 + registerOnTouched(fn: any): void {
  76 + }
  77 +
  78 + ngOnInit() {
  79 + this.smsProviderConfigurationFormGroup = this.fb.group({
  80 + type: [null, Validators.required],
  81 + configuration: [null, Validators.required]
  82 + });
  83 + this.smsProviderConfigurationFormGroup.valueChanges.subscribe(() => {
  84 + this.updateModel();
  85 + });
  86 + this.smsProviderConfigurationFormGroup.get('type').valueChanges.subscribe(() => {
  87 + this.smsProviderTypeChanged();
  88 + });
  89 + }
  90 +
  91 + setDisabledState(isDisabled: boolean): void {
  92 + this.disabled = isDisabled;
  93 + if (this.disabled) {
  94 + this.smsProviderConfigurationFormGroup.disable({emitEvent: false});
  95 + } else {
  96 + this.smsProviderConfigurationFormGroup.enable({emitEvent: false});
  97 + }
  98 + }
  99 +
  100 + writeValue(value: SmsProviderConfiguration | null): void {
  101 + const configuration = deepClone(value);
  102 + const type = configuration?.type;
  103 + if (configuration) {
  104 + delete configuration.type;
  105 + }
  106 + this.smsProviderConfigurationFormGroup.patchValue({type}, {emitEvent: false});
  107 + this.smsProviderConfigurationFormGroup.patchValue({configuration}, {emitEvent: false});
  108 + }
  109 +
  110 + private smsProviderTypeChanged() {
  111 + const type: SmsProviderType = this.smsProviderConfigurationFormGroup.get('type').value;
  112 + this.smsProviderConfigurationFormGroup.patchValue({configuration: createSmsProviderConfiguration(type)}, {emitEvent: false});
  113 + }
  114 +
  115 + private updateModel() {
  116 + let configuration: SmsProviderConfiguration = null;
  117 + if (this.smsProviderConfigurationFormGroup.valid) {
  118 + configuration = this.smsProviderConfigurationFormGroup.getRawValue().configuration;
  119 + configuration.type = this.smsProviderConfigurationFormGroup.getRawValue().type;
  120 + }
  121 + this.propagateChange(configuration);
  122 + }
  123 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form [formGroup]="twilioSmsProviderConfigurationFormGroup" style="padding-bottom: 16px;">
  19 + <mat-form-field class="mat-block">
  20 + <mat-label translate>admin.number-from</mat-label>
  21 + <input type="tel" required [pattern]="phoneNumberPattern" matInput formControlName="numberFrom">
  22 + <mat-error *ngIf="twilioSmsProviderConfigurationFormGroup.get('numberFrom').hasError('required')">
  23 + {{ 'admin.number-from-required' | translate }}
  24 + </mat-error>
  25 + <mat-error *ngIf="twilioSmsProviderConfigurationFormGroup.get('numberFrom').hasError('pattern')">
  26 + {{ 'admin.phone-number-pattern' | translate }}
  27 + </mat-error>
  28 + <mat-hint innerHTML="{{ 'admin.phone-number-hint' | translate }}"></mat-hint>
  29 + </mat-form-field>
  30 + <mat-form-field class="mat-block">
  31 + <mat-label translate>admin.twilio-account-sid</mat-label>
  32 + <input required matInput formControlName="accountSid">
  33 + <mat-error *ngIf="twilioSmsProviderConfigurationFormGroup.get('accountSid').hasError('required')">
  34 + {{ 'admin.twilio-account-sid-required' | translate }}
  35 + </mat-error>
  36 + </mat-form-field>
  37 + <mat-form-field class="mat-block">
  38 + <mat-label translate>admin.twilio-account-token</mat-label>
  39 + <input required type="password" matInput formControlName="accountToken">
  40 + <mat-error *ngIf="twilioSmsProviderConfigurationFormGroup.get('accountToken').hasError('required')">
  41 + {{ 'admin.twilio-account-token-required' | translate }}
  42 + </mat-error>
  43 + </mat-form-field>
  44 +</form>
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, forwardRef, Input, OnInit } from '@angular/core';
  18 +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@app/core/core.state';
  21 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  22 +import { isDefinedAndNotNull } from '@core/utils';
  23 +import { phoneNumberPattern, TwilioSmsProviderConfiguration } from '@shared/models/settings.models';
  24 +
  25 +@Component({
  26 + selector: 'tb-twilio-sms-provider-configuration',
  27 + templateUrl: './twilio-sms-provider-configuration.component.html',
  28 + styleUrls: [],
  29 + providers: [{
  30 + provide: NG_VALUE_ACCESSOR,
  31 + useExisting: forwardRef(() => TwilioSmsProviderConfigurationComponent),
  32 + multi: true
  33 + }]
  34 +})
  35 +export class TwilioSmsProviderConfigurationComponent implements ControlValueAccessor, OnInit {
  36 +
  37 + twilioSmsProviderConfigurationFormGroup: FormGroup;
  38 +
  39 + phoneNumberPattern = phoneNumberPattern;
  40 +
  41 + private requiredValue: boolean;
  42 +
  43 + get required(): boolean {
  44 + return this.requiredValue;
  45 + }
  46 +
  47 + @Input()
  48 + set required(value: boolean) {
  49 + this.requiredValue = coerceBooleanProperty(value);
  50 + }
  51 +
  52 + @Input()
  53 + disabled: boolean;
  54 +
  55 + private propagateChange = (v: any) => { };
  56 +
  57 + constructor(private store: Store<AppState>,
  58 + private fb: FormBuilder) {
  59 + }
  60 +
  61 + registerOnChange(fn: any): void {
  62 + this.propagateChange = fn;
  63 + }
  64 +
  65 + registerOnTouched(fn: any): void {
  66 + }
  67 +
  68 + ngOnInit() {
  69 + this.twilioSmsProviderConfigurationFormGroup = this.fb.group({
  70 + numberFrom: [null, [Validators.required, Validators.pattern(phoneNumberPattern)]],
  71 + accountSid: [null, [Validators.required]],
  72 + accountToken: [null, [Validators.required]]
  73 + });
  74 + this.twilioSmsProviderConfigurationFormGroup.valueChanges.subscribe(() => {
  75 + this.updateModel();
  76 + });
  77 + }
  78 +
  79 + setDisabledState(isDisabled: boolean): void {
  80 + this.disabled = isDisabled;
  81 + if (this.disabled) {
  82 + this.twilioSmsProviderConfigurationFormGroup.disable({emitEvent: false});
  83 + } else {
  84 + this.twilioSmsProviderConfigurationFormGroup.enable({emitEvent: false});
  85 + }
  86 + }
  87 +
  88 + writeValue(value: TwilioSmsProviderConfiguration | null): void {
  89 + if (isDefinedAndNotNull(value)) {
  90 + this.twilioSmsProviderConfigurationFormGroup.patchValue(value, {emitEvent: false});
  91 + }
  92 + }
  93 +
  94 + private updateModel() {
  95 + let configuration: TwilioSmsProviderConfiguration = null;
  96 + if (this.twilioSmsProviderConfigurationFormGroup.valid) {
  97 + configuration = this.twilioSmsProviderConfigurationFormGroup.value;
  98 + }
  99 + this.propagateChange(configuration);
  100 + }
  101 +}
@@ -31,6 +31,7 @@ import { Observable } from 'rxjs'; @@ -31,6 +31,7 @@ import { Observable } from 'rxjs';
31 import { getCurrentAuthUser } from '@core/auth/auth.selectors'; 31 import { getCurrentAuthUser } from '@core/auth/auth.selectors';
32 import { OAuth2Service } from '@core/http/oauth2.service'; 32 import { OAuth2Service } from '@core/http/oauth2.service';
33 import { UserProfileResolver } from '@home/pages/profile/profile-routing.module'; 33 import { UserProfileResolver } from '@home/pages/profile/profile-routing.module';
  34 +import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
34 35
35 @Injectable() 36 @Injectable()
36 export class OAuth2LoginProcessingUrlResolver implements Resolve<string> { 37 export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@@ -86,6 +87,19 @@ const routes: Routes = [ @@ -86,6 +87,19 @@ const routes: Routes = [
86 } 87 }
87 }, 88 },
88 { 89 {
  90 + path: 'sms-provider',
  91 + component: SmsProviderComponent,
  92 + canDeactivate: [ConfirmOnExitGuard],
  93 + data: {
  94 + auth: [Authority.SYS_ADMIN],
  95 + title: 'admin.sms-provider-settings',
  96 + breadcrumb: {
  97 + label: 'admin.sms-provider',
  98 + icon: 'sms'
  99 + }
  100 + }
  101 + },
  102 + {
89 path: 'security-settings', 103 path: 'security-settings',
90 component: SecuritySettingsComponent, 104 component: SecuritySettingsComponent,
91 canDeactivate: [ConfirmOnExitGuard], 105 canDeactivate: [ConfirmOnExitGuard],
@@ -24,12 +24,16 @@ import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-sett @@ -24,12 +24,16 @@ import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-sett
24 import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component'; 24 import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component';
25 import { HomeComponentsModule } from '@modules/home/components/home-components.module'; 25 import { HomeComponentsModule } from '@modules/home/components/home-components.module';
26 import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component'; 26 import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component';
  27 +import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
  28 +import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component';
27 29
28 @NgModule({ 30 @NgModule({
29 declarations: 31 declarations:
30 [ 32 [
31 GeneralSettingsComponent, 33 GeneralSettingsComponent,
32 MailServerComponent, 34 MailServerComponent,
  35 + SmsProviderComponent,
  36 + SendTestSmsDialogComponent,
33 SecuritySettingsComponent, 37 SecuritySettingsComponent,
34 OAuth2SettingsComponent 38 OAuth2SettingsComponent
35 ], 39 ],
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form [formGroup]="sendTestSmsFormGroup" style="min-width: 500px; position: relative;">
  19 + <mat-toolbar color="primary">
  20 + <h2 translate>admin.send-test-sms</h2>
  21 + <span fxFlex></span>
  22 + <button mat-icon-button
  23 + (click)="close()"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  31 + <div mat-dialog-content tb-toast toastTarget="sendTestSmsDialogContent">
  32 + <fieldset [disabled]="(isLoading$ | async)">
  33 + <mat-form-field class="mat-block">
  34 + <mat-label translate>admin.number-to</mat-label>
  35 + <input type="tel" required [pattern]="phoneNumberPattern" matInput formControlName="numberTo">
  36 + <mat-error *ngIf="sendTestSmsFormGroup.get('numberTo').hasError('required')">
  37 + {{ 'admin.number-to-required' | translate }}
  38 + </mat-error>
  39 + <mat-error *ngIf="sendTestSmsFormGroup.get('numberTo').hasError('pattern')">
  40 + {{ 'admin.phone-number-pattern' | translate }}
  41 + </mat-error>
  42 + <mat-hint innerHTML="{{ 'admin.phone-number-hint' | translate }}"></mat-hint>
  43 + </mat-form-field>
  44 + <mat-form-field class="mat-block">
  45 + <mat-label translate>admin.sms-message</mat-label>
  46 + <textarea required matInput rows="3" [maxLength]="1600" formControlName="message"></textarea>
  47 + <mat-error *ngIf="sendTestSmsFormGroup.get('message').hasError('required')">
  48 + {{ 'admin.sms-message-required' | translate }}
  49 + </mat-error>
  50 + <mat-error *ngIf="sendTestSmsFormGroup.get('message').hasError('maxLength')">
  51 + {{ 'admin.sms-message-max-length' | translate }}
  52 + </mat-error>
  53 + </mat-form-field>
  54 + </fieldset>
  55 + </div>
  56 + <div mat-dialog-actions fxLayoutAlign="end center">
  57 + <button mat-button color="primary"
  58 + type="button"
  59 + [disabled]="(isLoading$ | async)"
  60 + (click)="close()" cdkFocusInitial>
  61 + {{ 'action.close' | translate }}
  62 + </button>
  63 + <button mat-raised-button color="primary"
  64 + type="button"
  65 + (click)="sendTestSms()"
  66 + [disabled]="(isLoading$ | async) || sendTestSmsFormGroup.invalid">
  67 + {{ 'admin.send-test-sms' | translate }}
  68 + </button>
  69 + </div>
  70 +</form>
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, OnInit } from '@angular/core';
  18 +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  22 +import { DialogComponent } from '@shared/components/dialog.component';
  23 +import { Router } from '@angular/router';
  24 +import { phoneNumberPattern, SmsProviderConfiguration, TestSmsRequest } from '@shared/models/settings.models';
  25 +import { AdminService } from '@core/http/admin.service';
  26 +import { ActionNotificationShow } from '@core/notification/notification.actions';
  27 +import { TranslateService } from '@ngx-translate/core';
  28 +
  29 +export interface SendTestSmsDialogData {
  30 + smsProviderConfiguration: SmsProviderConfiguration;
  31 +}
  32 +
  33 +@Component({
  34 + selector: 'tb-send-test-sms-dialog',
  35 + templateUrl: './send-test-sms-dialog.component.html',
  36 + styleUrls: []
  37 +})
  38 +export class SendTestSmsDialogComponent extends
  39 + DialogComponent<SendTestSmsDialogComponent> implements OnInit {
  40 +
  41 + phoneNumberPattern = phoneNumberPattern;
  42 +
  43 + sendTestSmsFormGroup: FormGroup;
  44 +
  45 + smsProviderConfiguration = this.data.smsProviderConfiguration;
  46 +
  47 + constructor(protected store: Store<AppState>,
  48 + protected router: Router,
  49 + @Inject(MAT_DIALOG_DATA) public data: SendTestSmsDialogData,
  50 + private adminService: AdminService,
  51 + private translate: TranslateService,
  52 + public dialogRef: MatDialogRef<SendTestSmsDialogComponent>,
  53 + public fb: FormBuilder) {
  54 + super(store, router, dialogRef);
  55 + }
  56 +
  57 + ngOnInit(): void {
  58 + this.sendTestSmsFormGroup = this.fb.group({
  59 + numberTo: [null, [Validators.required, Validators.pattern(phoneNumberPattern)]],
  60 + message: [null, [Validators.required, Validators.maxLength(1600)]]
  61 + });
  62 + }
  63 +
  64 + close(): void {
  65 + this.dialogRef.close();
  66 + }
  67 +
  68 + sendTestSms(): void {
  69 + const request: TestSmsRequest = {
  70 + providerConfiguration: this.smsProviderConfiguration,
  71 + numberTo: this.sendTestSmsFormGroup.value.numberTo,
  72 + message: this.sendTestSmsFormGroup.value.message
  73 + };
  74 + this.adminService.sendTestSms(request).subscribe(
  75 + () => {
  76 + this.store.dispatch(new ActionNotificationShow(
  77 + {
  78 + message: this.translate.instant('admin.test-sms-sent'),
  79 + target: 'sendTestSmsDialogContent',
  80 + verticalPosition: 'bottom',
  81 + horizontalPosition: 'left',
  82 + type: 'success'
  83 + }));
  84 + }
  85 + );
  86 + }
  87 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div>
  19 + <mat-card class="settings-card">
  20 + <mat-card-title>
  21 + <div fxLayout="row">
  22 + <span class="mat-headline" translate>admin.sms-provider-settings</span>
  23 + <span fxFlex></span>
  24 + <div tb-help="smsProviderSettings"></div>
  25 + </div>
  26 + </mat-card-title>
  27 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  28 + </mat-progress-bar>
  29 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  30 + <mat-card-content style="padding-top: 16px;">
  31 + <form [formGroup]="smsProvider" (ngSubmit)="save()">
  32 + <fieldset [disabled]="isLoading$ | async">
  33 + <tb-sms-provider-configuration
  34 + required
  35 + formControlName="configuration">
  36 + </tb-sms-provider-configuration>
  37 + <div fxLayout="row" fxLayoutAlign="end center" fxLayout.xs="column" fxLayoutAlign.xs="end" fxLayoutGap="16px">
  38 + <button mat-raised-button type="button"
  39 + [disabled]="(isLoading$ | async) || smsProvider.invalid" (click)="sendTestSms()">
  40 + {{'admin.send-test-sms' | translate}}
  41 + </button>
  42 + <button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || smsProvider.invalid || !smsProvider.dirty"
  43 + type="submit">{{'action.save' | translate}}
  44 + </button>
  45 + </div>
  46 + </fieldset>
  47 + </form>
  48 + </mat-card-content>
  49 + </mat-card>
  50 +</div>
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 +
  18 +}
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnInit } from '@angular/core';
  18 +import { Store } from '@ngrx/store';
  19 +import { AppState } from '@core/core.state';
  20 +import { PageComponent } from '@shared/components/page.component';
  21 +import { Router } from '@angular/router';
  22 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  23 +import { AdminSettings, SmsProviderConfiguration } from '@shared/models/settings.models';
  24 +import { AdminService } from '@core/http/admin.service';
  25 +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
  26 +import { MatDialog } from '@angular/material/dialog';
  27 +import { SendTestSmsDialogComponent, SendTestSmsDialogData } from '@home/pages/admin/send-test-sms-dialog.component';
  28 +
  29 +@Component({
  30 + selector: 'tb-sms-provider',
  31 + templateUrl: './sms-provider.component.html',
  32 + styleUrls: ['./sms-provider.component.scss', './settings-card.scss']
  33 +})
  34 +export class SmsProviderComponent extends PageComponent implements OnInit, HasConfirmForm {
  35 +
  36 + smsProvider: FormGroup;
  37 + adminSettings: AdminSettings<SmsProviderConfiguration>;
  38 +
  39 + constructor(protected store: Store<AppState>,
  40 + private router: Router,
  41 + private adminService: AdminService,
  42 + private dialog: MatDialog,
  43 + public fb: FormBuilder) {
  44 + super(store);
  45 + }
  46 +
  47 + ngOnInit() {
  48 + this.buildSmsProviderForm();
  49 + this.adminService.getAdminSettings<SmsProviderConfiguration>('sms', {ignoreErrors: true}).subscribe(
  50 + (adminSettings) => {
  51 + this.adminSettings = adminSettings;
  52 + this.smsProvider.reset({configuration: this.adminSettings.jsonValue});
  53 + },
  54 + () => {
  55 + this.adminSettings = {
  56 + key: 'sms',
  57 + jsonValue: null
  58 + };
  59 + this.smsProvider.reset({configuration: this.adminSettings.jsonValue});
  60 + }
  61 + );
  62 + }
  63 +
  64 + buildSmsProviderForm() {
  65 + this.smsProvider = this.fb.group({
  66 + configuration: [null, [Validators.required]]
  67 + });
  68 + this.registerDisableOnLoadFormControl(this.smsProvider.get('configuration'));
  69 + }
  70 +
  71 + sendTestSms(): void {
  72 + this.dialog.open<SendTestSmsDialogComponent, SendTestSmsDialogData>(SendTestSmsDialogComponent, {
  73 + disableClose: true,
  74 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  75 + data: {
  76 + smsProviderConfiguration: this.smsProvider.value.configuration
  77 + }
  78 + });
  79 + }
  80 +
  81 + save(): void {
  82 + this.adminSettings.jsonValue = this.smsProvider.value.configuration;
  83 + this.adminService.saveAdminSettings(this.adminSettings).subscribe(
  84 + (adminSettings) => {
  85 + this.adminSettings = adminSettings;
  86 + this.smsProvider.reset({configuration: this.adminSettings.jsonValue});
  87 + }
  88 + );
  89 + }
  90 +
  91 + confirmForm(): FormGroup {
  92 + return this.smsProvider;
  93 + }
  94 +
  95 +}
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div style="min-width: 400px;"> 18 +<div style="min-width: 400px; position: relative;">
19 <mat-toolbar fxLayout="row" color="primary"> 19 <mat-toolbar fxLayout="row" color="primary">
20 <h2>{{ 'dashboard.public-dashboard-title' | translate }}</h2> 20 <h2>{{ 'dashboard.public-dashboard-title' | translate }}</h2>
21 <span fxFlex></span> 21 <span fxFlex></span>
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<form style="min-width: 400px;"> 18 +<form style="min-width: 400px; position: relative;">
19 <mat-toolbar color="primary"> 19 <mat-toolbar color="primary">
20 <h2 translate>user.activation-link</h2> 20 <h2 translate>user.activation-link</h2>
21 <span fxFlex></span> 21 <span fxFlex></span>
@@ -59,6 +59,7 @@ const helpBaseUrl = 'https://thingsboard.io'; @@ -59,6 +59,7 @@ const helpBaseUrl = 'https://thingsboard.io';
59 export const HelpLinks = { 59 export const HelpLinks = {
60 linksMap: { 60 linksMap: {
61 outgoingMailSettings: helpBaseUrl + '/docs/user-guide/ui/mail-settings', 61 outgoingMailSettings: helpBaseUrl + '/docs/user-guide/ui/mail-settings',
  62 + smsProviderSettings: helpBaseUrl + '/docs/user-guide/ui/sms-provider-settings',
62 securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings', 63 securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings',
63 oauth2Settings: helpBaseUrl + '/docs/user-guide/oauth-2-support/', 64 oauth2Settings: helpBaseUrl + '/docs/user-guide/oauth-2-support/',
64 ruleEngine: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/overview/', 65 ruleEngine: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/overview/',
@@ -14,6 +14,8 @@ @@ -14,6 +14,8 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
  17 +import { DeviceTransportType } from '@shared/models/device.models';
  18 +
17 export const smtpPortPattern: RegExp = /^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/; 19 export const smtpPortPattern: RegExp = /^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/;
18 20
19 export interface AdminSettings<T> { 21 export interface AdminSettings<T> {
@@ -60,3 +62,66 @@ export interface UpdateMessage { @@ -60,3 +62,66 @@ export interface UpdateMessage {
60 message: string; 62 message: string;
61 updateAvailable: boolean; 63 updateAvailable: boolean;
62 } 64 }
  65 +
  66 +export const phoneNumberPattern = /^\+[1-9]\d{1,14}$/;
  67 +
  68 +export enum SmsProviderType {
  69 + AWS_SNS = 'AWS_SNS',
  70 + TWILIO = 'TWILIO'
  71 +}
  72 +
  73 +export const smsProviderTypeTranslationMap = new Map<SmsProviderType, string>(
  74 + [
  75 + [SmsProviderType.AWS_SNS, 'admin.sms-provider-type-aws-sns'],
  76 + [SmsProviderType.TWILIO, 'admin.sms-provider-type-twilio']
  77 + ]
  78 +);
  79 +
  80 +export interface AwsSnsSmsProviderConfiguration {
  81 + accessKeyId?: string;
  82 + secretAccessKey?: string;
  83 + region?: string;
  84 +}
  85 +
  86 +export interface TwilioSmsProviderConfiguration {
  87 + accountSid?: string;
  88 + accountToken?: string;
  89 + numberFrom?: string;
  90 +}
  91 +
  92 +export type SmsProviderConfigurations = AwsSnsSmsProviderConfiguration & TwilioSmsProviderConfiguration;
  93 +
  94 +export interface SmsProviderConfiguration extends SmsProviderConfigurations {
  95 + type: SmsProviderType;
  96 +}
  97 +
  98 +export interface TestSmsRequest {
  99 + providerConfiguration: SmsProviderConfiguration;
  100 + numberTo: string;
  101 + message: string;
  102 +}
  103 +
  104 +export function createSmsProviderConfiguration(type: SmsProviderType): SmsProviderConfiguration {
  105 + let smsProviderConfiguration: SmsProviderConfiguration;
  106 + if (type) {
  107 + switch (type) {
  108 + case SmsProviderType.AWS_SNS:
  109 + const awsSnsSmsProviderConfiguration: AwsSnsSmsProviderConfiguration = {
  110 + accessKeyId: '',
  111 + secretAccessKey: '',
  112 + region: 'us-east-1'
  113 + };
  114 + smsProviderConfiguration = {...awsSnsSmsProviderConfiguration, type: SmsProviderType.AWS_SNS};
  115 + break;
  116 + case SmsProviderType.TWILIO:
  117 + const twilioSmsProviderConfiguration: TwilioSmsProviderConfiguration = {
  118 + numberFrom: '',
  119 + accountSid: '',
  120 + accountToken: ''
  121 + };
  122 + smsProviderConfiguration = {...twilioSmsProviderConfiguration, type: SmsProviderType.TWILIO};
  123 + break;
  124 + }
  125 + }
  126 + return smsProviderConfiguration;
  127 +}
@@ -104,6 +104,33 @@ @@ -104,6 +104,33 @@
104 "proxy-user": "Proxy user", 104 "proxy-user": "Proxy user",
105 "proxy-password": "Proxy password", 105 "proxy-password": "Proxy password",
106 "send-test-mail": "Send test mail", 106 "send-test-mail": "Send test mail",
  107 + "sms-provider": "SMS provider",
  108 + "sms-provider-settings": "SMS provider settings",
  109 + "sms-provider-type": "SMS provider type",
  110 + "sms-provider-type-required": "SMS provider type is required.",
  111 + "sms-provider-type-aws-sns": "Amazon SNS",
  112 + "sms-provider-type-twilio": "Twilio",
  113 + "aws-access-key-id": "AWS Access Key ID",
  114 + "aws-access-key-id-required": "AWS Access Key ID is required",
  115 + "aws-secret-access-key": "AWS Secret Access Key",
  116 + "aws-secret-access-key-required": "AWS Secret Access Key is required",
  117 + "aws-region": "AWS Region",
  118 + "aws-region-required": "AWS Region is required",
  119 + "number-from": "Phone Number From",
  120 + "number-from-required": "Phone Number From is required.",
  121 + "number-to": "Phone Number To",
  122 + "number-to-required": "Phone Number To is required.",
  123 + "phone-number-hint": "Phone Number in E.164 format, ex. +19995550123",
  124 + "phone-number-pattern": "Invalid phone number. Should be in E.164 format, ex. +19995550123.",
  125 + "sms-message": "SMS message",
  126 + "sms-message-required": "SMS message is required.",
  127 + "sms-message-max-length": "SMS message can't be longer 1600 characters",
  128 + "twilio-account-sid": "Twilio Account SID",
  129 + "twilio-account-sid-required": "Twilio Account SID is required",
  130 + "twilio-account-token": "Twilio Account Token",
  131 + "twilio-account-token-required": "Twilio Account Token is required",
  132 + "send-test-sms": "Send test SMS",
  133 + "test-sms-sent": "Test SMS was successfully sent!",
107 "security-settings": "Security settings", 134 "security-settings": "Security settings",
108 "password-policy": "Password policy", 135 "password-policy": "Password policy",
109 "minimum-password-length": "Minimum password length", 136 "minimum-password-length": "Minimum password length",