Browse Source

Introduce SMS Service. Add Send SMS Rule Node

pull/3760/head
Igor Kulikov 6 years ago
parent
commit
1e1e3ec6a3
  1. 8
      application/pom.xml
  2. 19
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  3. 21
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  4. 18
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  5. 54
      application/src/main/java/org/thingsboard/server/service/sms/AbstractSmsSender.java
  6. 42
      application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java
  7. 127
      application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java
  8. 33
      application/src/main/java/org/thingsboard/server/service/sms/SmsExecutorService.java
  9. 73
      application/src/main/java/org/thingsboard/server/service/sms/aws/AwsSmsSender.java
  10. 56
      application/src/main/java/org/thingsboard/server/service/sms/twilio/TwilioSmsSender.java
  11. 4
      application/src/main/resources/thingsboard.yml
  12. 29
      pom.xml
  13. 31
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/SmsService.java
  14. 7
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  15. 26
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/SmsSender.java
  16. 24
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/SmsSenderFactory.java
  17. 32
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/config/AwsSnsSmsProviderConfiguration.java
  18. 36
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/config/SmsProviderConfiguration.java
  19. 21
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/config/SmsProviderType.java
  20. 27
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/config/TestSmsRequest.java
  21. 32
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/config/TwilioSmsProviderConfiguration.java
  22. 28
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/exception/SmsException.java
  23. 28
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/exception/SmsParseException.java
  24. 27
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/exception/SmsSendException.java
  25. 97
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java
  26. 38
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNodeConfiguration.java
  27. 13
      ui-ngx/src/app/core/http/admin.service.ts
  28. 14
      ui-ngx/src/app/core/services/menu.service.ts
  29. 78
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  30. 40
      ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html
  31. 99
      ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.ts
  32. 44
      ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html
  33. 123
      ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.ts
  34. 44
      ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.html
  35. 101
      ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.ts
  36. 14
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  37. 4
      ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
  38. 70
      ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.html
  39. 87
      ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.ts
  40. 50
      ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html
  41. 18
      ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.scss
  42. 95
      ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.ts
  43. 2
      ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html
  44. 2
      ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html
  45. 1
      ui-ngx/src/app/shared/models/constants.ts
  46. 65
      ui-ngx/src/app/shared/models/settings.models.ts
  47. 27
      ui-ngx/src/assets/locale/locale.constant-en_US.json

8
application/pom.xml

@ -193,6 +193,14 @@
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
</dependency>
<dependency>
<groupId>com.twilio.sdk</groupId>
<artifactId>twilio</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sns</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>

19
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -32,6 +32,8 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.common.data.DataConstants;
@ -80,6 +82,7 @@ import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
import org.thingsboard.server.service.script.JsExecutorService;
import org.thingsboard.server.service.script.JsInvokeService;
import org.thingsboard.server.service.session.DeviceSessionCacheService;
import org.thingsboard.server.service.sms.SmsExecutorService;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
@ -228,6 +231,10 @@ public class ActorSystemContext {
@Getter
private MailExecutorService mailExecutor;
@Autowired
@Getter
private SmsExecutorService smsExecutor;
@Autowired
@Getter
private DbCallbackExecutorService dbCallbackExecutor;
@ -244,6 +251,14 @@ public class ActorSystemContext {
@Getter
private MailService mailService;
@Autowired
@Getter
private SmsService smsService;
@Autowired
@Getter
private SmsSenderFactory smsSenderFactory;
@Autowired
@Getter
private ClaimDevicesService claimDevicesService;
@ -325,6 +340,10 @@ public class ActorSystemContext {
@Getter
private boolean allowSystemMailService;
@Value("${actors.rule.allow_system_sms_service}")
@Getter
private boolean allowSystemSmsService;
@Value("${transport.sessions.inactivity_timeout}")
@Getter
private long sessionInactivityTimeout;

21
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -28,8 +28,10 @@ import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
import org.thingsboard.rule.engine.api.RuleEngineRpcService;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbRelationTypes;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.common.data.Customer;
@ -302,6 +304,11 @@ class DefaultTbContext implements TbContext {
return mainCtx.getMailExecutor();
}
@Override
public ListeningExecutor getSmsExecutor() {
return mainCtx.getSmsExecutor();
}
@Override
public ListeningExecutor getDbCallbackExecutor() {
return mainCtx.getDbCallbackExecutor();
@ -427,6 +434,20 @@ class DefaultTbContext implements TbContext {
}
}
@Override
public SmsService getSmsService() {
if (mainCtx.isAllowSystemSmsService()) {
return mainCtx.getSmsService();
} else {
throw new RuntimeException("Access to System SMS Service is forbidden!");
}
}
@Override
public SmsSenderFactory getSmsSenderFactory() {
return mainCtx.getSmsSenderFactory();
}
@Override
public RuleEngineRpcService getRpcService() {
return mainCtx.getTbRuleEngineDeviceRpcService();

18
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -25,6 +25,8 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -45,6 +47,9 @@ public class AdminController extends BaseController {
@Autowired
private MailService mailService;
@Autowired
private SmsService smsService;
@Autowired
private AdminSettingsService adminSettingsService;
@ -80,6 +85,8 @@ public class AdminController extends BaseController {
if (adminSettings.getKey().equals("mail")) {
mailService.updateMailConfiguration();
((ObjectNode) adminSettings.getJsonValue()).put("password", "");
} else if (adminSettings.getKey().equals("sms")) {
smsService.updateSmsConfiguration();
}
return adminSettings;
} catch (Exception e) {
@ -127,6 +134,17 @@ public class AdminController extends BaseController {
}
}
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/settings/testSms", method = RequestMethod.POST)
public void sendTestSms(@RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
smsService.sendTestSms(testSmsRequest);
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/updates", method = RequestMethod.GET)
@ResponseBody

54
application/src/main/java/org/thingsboard/server/service/sms/AbstractSmsSender.java

@ -0,0 +1,54 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sms;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.sms.SmsSender;
import org.thingsboard.rule.engine.api.sms.exception.SmsParseException;
import sun.misc.Regexp;
import java.util.regex.Pattern;
@Slf4j
public abstract class AbstractSmsSender implements SmsSender {
private static final Pattern E_164_PHONE_NUMBER_PATTERN = Pattern.compile("^\\+[1-9]\\d{1,14}$");
private static final int MAX_SMS_MESSAGE_LENGTH = 1600;
private static final int MAX_SMS_SEGMENT_LENGTH = 70;
protected String validatePhoneNumber(String phoneNumber) throws SmsParseException {
phoneNumber = phoneNumber.trim();
if (!E_164_PHONE_NUMBER_PATTERN.matcher(phoneNumber).matches()) {
throw new SmsParseException("Invalid phone number format. Phone number must be in E.164 format.");
}
return phoneNumber;
}
protected String prepareMessage(String message) {
message = message.replaceAll("^\"|\"$", "").replaceAll("\\\\n", "\n");
if (message.length() > MAX_SMS_MESSAGE_LENGTH) {
log.warn("SMS message exceeds maximum symbols length and will be truncated");
message = message.substring(0, MAX_SMS_MESSAGE_LENGTH);
}
return message;
}
protected int countMessageSegments(String message) {
return (int)Math.ceil((double) message.length() / (double) MAX_SMS_SEGMENT_LENGTH);
}
}

42
application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sms;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.sms.SmsSender;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.rule.engine.api.sms.config.AwsSnsSmsProviderConfiguration;
import org.thingsboard.rule.engine.api.sms.config.SmsProviderConfiguration;
import org.thingsboard.rule.engine.api.sms.config.TwilioSmsProviderConfiguration;
import org.thingsboard.server.service.sms.aws.AwsSmsSender;
import org.thingsboard.server.service.sms.twilio.TwilioSmsSender;
@Component
public class DefaultSmsSenderFactory implements SmsSenderFactory {
@Override
public SmsSender createSmsSender(SmsProviderConfiguration config) {
switch (config.getType()) {
case AWS_SNS:
return new AwsSmsSender((AwsSnsSmsProviderConfiguration)config);
case TWILIO:
return new TwilioSmsSender((TwilioSmsProviderConfiguration)config);
default:
throw new RuntimeException("Unknown SMS provider type " + config.getType());
}
}
}

127
application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java

@ -0,0 +1,127 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sms;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.NestedRuntimeException;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.sms.SmsSender;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.rule.engine.api.sms.config.SmsProviderConfiguration;
import org.thingsboard.rule.engine.api.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.util.mapping.JacksonUtil;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@Service
@Slf4j
public class DefaultSmsService implements SmsService {
@Autowired
private SmsSenderFactory smsSenderFactory;
@Autowired
private AdminSettingsService adminSettingsService;
private SmsSender smsSender;
@PostConstruct
private void init() {
updateSmsConfiguration();
}
@PreDestroy
private void destroy() {
if (this.smsSender != null) {
this.smsSender.destroy();
}
}
@Override
public void updateSmsConfiguration() {
AdminSettings settings = adminSettingsService.findAdminSettingsByKey(new TenantId(EntityId.NULL_UUID), "sms");
if (settings != null) {
try {
JsonNode jsonConfig = settings.getJsonValue();
SmsProviderConfiguration configuration = JacksonUtil.convertValue(jsonConfig, SmsProviderConfiguration.class);
SmsSender newSmsSender = this.smsSenderFactory.createSmsSender(configuration);
if (this.smsSender != null) {
this.smsSender.destroy();
}
this.smsSender = newSmsSender;
} catch (Exception e) {
log.error("Failed to create SMS sender", e);
}
}
}
@Override
public void sendSms(String numberTo, String message) throws ThingsboardException {
if (this.smsSender == null) {
throw new ThingsboardException("Unable to send SMS: no SMS provider configured!", ThingsboardErrorCode.GENERAL);
}
this.sendSms(this.smsSender, numberTo, message);
}
@Override
public void sendSms(String[] numbersTo, String message) throws ThingsboardException {
for (String numberTo : numbersTo) {
this.sendSms(numberTo, message);
}
}
@Override
public void sendTestSms(TestSmsRequest testSmsRequest) throws ThingsboardException {
SmsSender testSmsSender;
try {
testSmsSender = this.smsSenderFactory.createSmsSender(testSmsRequest.getProviderConfiguration());
} catch (Exception e) {
throw handleException(e);
}
this.sendSms(testSmsSender, testSmsRequest.getNumberTo(), testSmsRequest.getMessage());
testSmsSender.destroy();
}
private int sendSms(SmsSender smsSender, String numberTo, String message) throws ThingsboardException {
try {
return smsSender.sendSms(numberTo, message);
} catch (Exception e) {
throw handleException(e);
}
}
private ThingsboardException handleException(Exception exception) {
String message;
if (exception instanceof NestedRuntimeException) {
message = ((NestedRuntimeException) exception).getMostSpecificCause().getMessage();
} else {
message = exception.getMessage();
}
log.warn("Unable to send SMS: {}", message);
return new ThingsboardException(String.format("Unable to send SMS: %s", message),
ThingsboardErrorCode.GENERAL);
}
}

33
application/src/main/java/org/thingsboard/server/service/sms/SmsExecutorService.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sms;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.AbstractListeningExecutor;
@Component
public class SmsExecutorService extends AbstractListeningExecutor {
@Value("${actors.rule.sms_thread_pool_size}")
private int smsExecutorThreadPoolSize;
@Override
protected int getThreadPollSize() {
return smsExecutorThreadPoolSize;
}
}

73
application/src/main/java/org/thingsboard/server/service/sms/aws/AwsSmsSender.java

@ -0,0 +1,73 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sms.aws;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.AmazonSNSClient;
import com.amazonaws.services.sns.model.PublishRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.rule.engine.api.sms.config.AwsSnsSmsProviderConfiguration;
import org.thingsboard.rule.engine.api.sms.exception.SmsException;
import org.thingsboard.rule.engine.api.sms.exception.SmsSendException;
import org.thingsboard.server.service.sms.AbstractSmsSender;
@Slf4j
public class AwsSmsSender extends AbstractSmsSender {
private AmazonSNS snsClient;
public AwsSmsSender(AwsSnsSmsProviderConfiguration config) {
if (StringUtils.isEmpty(config.getAccessKeyId()) || StringUtils.isEmpty(config.getSecretAccessKey()) || StringUtils.isEmpty(config.getRegion())) {
throw new IllegalArgumentException("Invalid AWS sms provider configuration: aws accessKeyId, aws secretAccessKey and aws region should be specified!");
}
AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKeyId(), config.getSecretAccessKey());
AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
this.snsClient = AmazonSNSClient.builder()
.withCredentials(credProvider)
.withRegion(config.getRegion())
.build();
}
@Override
public int sendSms(String numberTo, String message) throws SmsException {
numberTo = this.validatePhoneNumber(numberTo);
message = this.prepareMessage(message);
try {
PublishRequest publishRequest = new PublishRequest()
.withPhoneNumber(numberTo)
.withMessage(message);
this.snsClient.publish(publishRequest);
return this.countMessageSegments(message);
} catch (Exception e) {
throw new SmsSendException("Failed to send SMS message - " + e.getMessage(), e);
}
}
@Override
public void destroy() {
if (this.snsClient != null) {
try {
this.snsClient.shutdown();
} catch (Exception e) {
log.error("Failed to shutdown SNS client during destroy()", e);
}
}
}
}

56
application/src/main/java/org/thingsboard/server/service/sms/twilio/TwilioSmsSender.java

@ -0,0 +1,56 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sms.twilio;
import com.twilio.http.TwilioRestClient;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.rule.engine.api.sms.config.TwilioSmsProviderConfiguration;
import org.thingsboard.rule.engine.api.sms.exception.SmsException;
import org.thingsboard.rule.engine.api.sms.exception.SmsSendException;
import org.thingsboard.server.service.sms.AbstractSmsSender;
public class TwilioSmsSender extends AbstractSmsSender {
private TwilioRestClient twilioRestClient;
private String numberFrom;
public TwilioSmsSender(TwilioSmsProviderConfiguration config) {
if (StringUtils.isEmpty(config.getAccountSid()) || StringUtils.isEmpty(config.getAccountToken()) || StringUtils.isEmpty(config.getNumberFrom())) {
throw new IllegalArgumentException("Invalid twilio sms provider configuration: accountSid, accountToken and numberFrom should be specified!");
}
this.numberFrom = this.validatePhoneNumber(config.getNumberFrom());
this.twilioRestClient = new TwilioRestClient.Builder(config.getAccountSid(), config.getAccountToken()).build();
}
@Override
public int sendSms(String numberTo, String message) throws SmsException {
numberTo = this.validatePhoneNumber(numberTo);
message = this.prepareMessage(message);
try {
String numSegments = Message.creator(new PhoneNumber(numberTo), new PhoneNumber(this.numberFrom), message).create(this.twilioRestClient).getNumSegments();
return Integer.valueOf(numSegments);
} catch (Exception e) {
throw new SmsSendException("Failed to send SMS message - " + e.getMessage(), e);
}
}
@Override
public void destroy() {
}
}

4
application/src/main/resources/thingsboard.yml

@ -281,8 +281,12 @@ actors:
js_thread_pool_size: "${ACTORS_RULE_JS_THREAD_POOL_SIZE:50}"
# Specify thread pool size for mail sender executor service
mail_thread_pool_size: "${ACTORS_RULE_MAIL_THREAD_POOL_SIZE:50}"
# Specify thread pool size for sms sender executor service
sms_thread_pool_size: "${ACTORS_RULE_SMS_THREAD_POOL_SIZE:50}"
# Whether to allow usage of system mail service for rules
allow_system_mail_service: "${ACTORS_RULE_ALLOW_SYSTEM_MAIL_SERVICE:true}"
# Whether to allow usage of system sms service for rules
allow_system_sms_service: "${ACTORS_RULE_ALLOW_SYSTEM_SMS_SERVICE:true}"
# Specify thread pool size for external call service
external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:50}"
chain:

29
pom.xml

@ -97,7 +97,7 @@
<fst.version>2.57</fst.version>
<antlr.version>2.7.7</antlr.version>
<snakeyaml.version>1.27</snakeyaml.version>
<amazonaws.sqs.version>1.11.747</amazonaws.sqs.version>
<aws.sdk.version>1.11.747</aws.sdk.version>
<pubsub.client.version>1.105.0</pubsub.client.version>
<azure-servicebus.version>3.2.0</azure-servicebus.version>
<passay.version>1.5.0</passay.version>
@ -108,6 +108,7 @@
<micrometer.version>1.5.2</micrometer.version>
<protobuf-dynamic.version>1.0.2TB</protobuf-dynamic.version>
<wire-schema.version>3.4.0</wire-schema.version>
<twilio.version>7.54.2</twilio.version>
</properties>
<modules>
@ -1318,7 +1319,12 @@
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sqs</artifactId>
<version>${amazonaws.sqs.version}</version>
<version>${aws.sdk.version}</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sns</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
@ -1381,6 +1387,25 @@
<artifactId>wire-schema</artifactId>
<version>${wire-schema.version}</version>
</dependency>
<dependency>
<groupId>com.twilio.sdk</groupId>
<artifactId>twilio</artifactId>
<version>${twilio.version}</version>
<exclusions>
<exclusion>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</exclusion>
<exclusion>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
</exclusion>
<exclusion>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</dependencyManagement>

31
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/SmsService.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.api;
import org.thingsboard.rule.engine.api.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.exception.ThingsboardException;
public interface SmsService {
void updateSmsConfiguration();
void sendSms(String numberTo, String message) throws ThingsboardException;
void sendSms(String[] numbersTo, String message) throws ThingsboardException;;
void sendTestSms(TestSmsRequest testSmsRequest) throws ThingsboardException;
}

7
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.api;
import io.netty.channel.EventLoopGroup;
import org.springframework.data.redis.core.RedisTemplate;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
@ -194,12 +195,18 @@ public interface TbContext {
ListeningExecutor getMailExecutor();
ListeningExecutor getSmsExecutor();
ListeningExecutor getDbCallbackExecutor();
ListeningExecutor getExternalCallExecutor();
MailService getMailService();
SmsService getSmsService();
SmsSenderFactory getSmsSenderFactory();
ScriptEngine createJsScriptEngine(String script, String... argNames);
void logJsEvalRequest();

26
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/SmsSender.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.api.sms;
import org.thingsboard.rule.engine.api.sms.exception.SmsException;
public interface SmsSender {
int sendSms(String numberTo, String message) throws SmsException;
void destroy();
}

24
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/SmsSenderFactory.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.api.sms;
import org.thingsboard.rule.engine.api.sms.config.SmsProviderConfiguration;
public interface SmsSenderFactory {
SmsSender createSmsSender(SmsProviderConfiguration config);
}

32
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/config/AwsSnsSmsProviderConfiguration.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.api.sms.config;
import lombok.Data;
@Data
public class AwsSnsSmsProviderConfiguration implements SmsProviderConfiguration {
private String accessKeyId;
private String secretAccessKey;
private String region;
@Override
public SmsProviderType getType() {
return SmsProviderType.AWS_SNS;
}
}

36
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/config/SmsProviderConfiguration.java

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.api.sms.config;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = AwsSnsSmsProviderConfiguration.class, name = "AWS_SNS"),
@JsonSubTypes.Type(value = TwilioSmsProviderConfiguration.class, name = "TWILIO")})
public interface SmsProviderConfiguration {
@JsonIgnore
SmsProviderType getType();
}

21
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/config/SmsProviderType.java

@ -0,0 +1,21 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.api.sms.config;
public enum SmsProviderType {
AWS_SNS,
TWILIO
}

27
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/config/TestSmsRequest.java

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.api.sms.config;
import lombok.Data;
@Data
public class TestSmsRequest {
private SmsProviderConfiguration providerConfiguration;
private String numberTo;
private String message;
}

32
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/config/TwilioSmsProviderConfiguration.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.api.sms.config;
import lombok.Data;
@Data
public class TwilioSmsProviderConfiguration implements SmsProviderConfiguration {
private String accountSid;
private String accountToken;
private String numberFrom;
@Override
public SmsProviderType getType() {
return SmsProviderType.TWILIO;
}
}

28
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/exception/SmsException.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.api.sms.exception;
public abstract class SmsException extends RuntimeException {
public SmsException(String msg) {
super(msg);
}
public SmsException(String msg, Throwable cause) {
super(msg, cause);
}
}

28
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/exception/SmsParseException.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.api.sms.exception;
public class SmsParseException extends SmsException {
public SmsParseException(String msg) {
super(msg);
}
public SmsParseException(String msg, Throwable cause) {
super(msg, cause);
}
}

27
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/sms/exception/SmsSendException.java

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.api.sms.exception;
public class SmsSendException extends SmsException {
public SmsSendException(String msg) {
super(msg);
}
public SmsSendException(String msg, Throwable cause) {
super(msg, cause);
}
}

97
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java

@ -0,0 +1,97 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.sms;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.sms.SmsSender;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
@RuleNode(
type = ComponentType.EXTERNAL,
name = "send sms",
configClazz = TbSendSmsNodeConfiguration.class,
nodeDescription = "Sends SMS message via SMS provider.",
nodeDetails = "Will send SMS message by populating target phone numbers and sms message fields using values derived from message metadata.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeSendSmsConfig",
icon = "sms"
)
public class TbSendSmsNode implements TbNode {
private TbSendSmsNodeConfiguration config;
private SmsSender smsSender;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
try {
this.config = TbNodeUtils.convert(configuration, TbSendSmsNodeConfiguration.class);
if (!this.config.isUseSystemSmsSettings()) {
smsSender = createSmsSender(ctx);
}
} catch (Exception e) {
throw new TbNodeException(e);
}
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
try {
withCallback(ctx.getSmsExecutor().executeAsync(() -> {
sendSms(ctx, msg);
return null;
}),
ok -> ctx.tellSuccess(msg),
fail -> ctx.tellFailure(msg, fail));
} catch (Exception ex) {
ctx.tellFailure(msg, ex);
}
}
private void sendSms(TbContext ctx, TbMsg msg) throws Exception {
String numbersTo = TbNodeUtils.processPattern(this.config.getNumbersToTemplate(), msg.getMetaData());
String message = TbNodeUtils.processPattern(this.config.getSmsMessageTemplate(), msg.getMetaData());
String[] numbersToList = numbersTo.split(",");
if (this.config.isUseSystemSmsSettings()) {
ctx.getSmsService().sendSms(numbersToList, message);
} else {
for (String numberTo : numbersToList) {
this.smsSender.sendSms(numberTo, message);
}
}
}
@Override
public void destroy() {
if (this.smsSender != null) {
this.smsSender.destroy();
}
}
private SmsSender createSmsSender(TbContext ctx) {
return ctx.getSmsSenderFactory().createSmsSender(this.config.getSmsProviderConfiguration());
}
}

38
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNodeConfiguration.java

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.sms;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.rule.engine.api.sms.config.SmsProviderConfiguration;
@Data
public class TbSendSmsNodeConfiguration implements NodeConfiguration {
private String numbersToTemplate;
private String smsMessageTemplate;
private boolean useSystemSmsSettings;
private SmsProviderConfiguration smsProviderConfiguration;
@Override
public NodeConfiguration defaultConfiguration() {
TbSendSmsNodeConfiguration configuration = new TbSendSmsNodeConfiguration();
configuration.numbersToTemplate = "${userPhone}";
configuration.smsMessageTemplate = "Device ${deviceName} has high temperature ${temp}";
configuration.setUseSystemSmsSettings(true);
return configuration;
}
}

13
ui-ngx/src/app/core/http/admin.service.ts

@ -18,7 +18,13 @@ import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { AdminSettings, MailServerSettings, SecuritySettings, UpdateMessage } from '@shared/models/settings.models';
import {
AdminSettings,
MailServerSettings,
SecuritySettings,
TestSmsRequest,
UpdateMessage
} from '@shared/models/settings.models';
@Injectable({
providedIn: 'root'
@ -43,6 +49,11 @@ export class AdminService {
return this.http.post<void>('/api/admin/settings/testMail', adminSettings, defaultHttpOptionsFromConfig(config));
}
public sendTestSms(testSmsRequest: TestSmsRequest,
config?: RequestConfig): Observable<void> {
return this.http.post<void>('/api/admin/settings/testSms', testSmsRequest, defaultHttpOptionsFromConfig(config));
}
public getSecuritySettings(config?: RequestConfig): Observable<SecuritySettings> {
return this.http.get<SecuritySettings>(`/api/admin/securitySettings`, defaultHttpOptionsFromConfig(config));
}

14
ui-ngx/src/app/core/services/menu.service.ts

@ -108,7 +108,7 @@ export class MenuService {
name: 'admin.system-settings',
type: 'toggle',
path: '/settings',
height: '160px',
height: '200px',
icon: 'settings',
pages: [
{
@ -125,6 +125,13 @@ export class MenuService {
path: '/settings/outgoing-mail',
icon: 'mail'
},
{
id: guid(),
name: 'admin.sms-provider',
type: 'link',
path: '/settings/sms-provider',
icon: 'sms'
},
{
id: guid(),
name: 'admin.security-settings',
@ -187,6 +194,11 @@ export class MenuService {
icon: 'mail',
path: '/settings/outgoing-mail'
},
{
name: 'admin.sms-provider',
icon: 'sms',
path: '/settings/sms-provider'
},
{
name: 'admin.security-settings',
icon: 'security',

78
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -77,44 +77,47 @@ import { ComplexFilterPredicateDialogComponent } from '@home/components/filter/c
import { KeyFilterDialogComponent } from '@home/components/filter/key-filter-dialog.component';
import { FiltersDialogComponent } from '@home/components/filter/filters-dialog.component';
import { FilterDialogComponent } from '@home/components/filter/filter-dialog.component';
import { FilterSelectComponent } from './filter/filter-select.component';
import { FilterSelectComponent } from '@home/components/filter/filter-select.component';
import { FiltersEditComponent } from '@home/components/filter/filters-edit.component';
import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-panel.component';
import { UserFilterDialogComponent } from '@home/components/filter/user-filter-dialog.component';
import { FilterUserInfoComponent } from './filter/filter-user-info.component';
import { FilterUserInfoDialogComponent } from './filter/filter-user-info-dialog.component';
import { FilterPredicateValueComponent } from './filter/filter-predicate-value.component';
import { TenantProfileAutocompleteComponent } from './profile/tenant-profile-autocomplete.component';
import { TenantProfileComponent } from './profile/tenant-profile.component';
import { TenantProfileDialogComponent } from './profile/tenant-profile-dialog.component';
import { TenantProfileDataComponent } from './profile/tenant-profile-data.component';
import { DefaultDeviceProfileConfigurationComponent } from './profile/device/default-device-profile-configuration.component';
import { DeviceProfileConfigurationComponent } from './profile/device/device-profile-configuration.component';
import { DeviceProfileComponent } from './profile/device-profile.component';
import { DefaultDeviceProfileTransportConfigurationComponent } from './profile/device/default-device-profile-transport-configuration.component';
import { DeviceProfileTransportConfigurationComponent } from './profile/device/device-profile-transport-configuration.component';
import { DeviceProfileDialogComponent } from './profile/device-profile-dialog.component';
import { DeviceProfileAutocompleteComponent } from './profile/device-profile-autocomplete.component';
import { MqttDeviceProfileTransportConfigurationComponent } from './profile/device/mqtt-device-profile-transport-configuration.component';
import { Lwm2mDeviceProfileTransportConfigurationComponent } from './profile/device/lwm2m-device-profile-transport-configuration.component';
import { DeviceProfileAlarmsComponent } from './profile/alarm/device-profile-alarms.component';
import { DeviceProfileAlarmComponent } from './profile/alarm/device-profile-alarm.component';
import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component';
import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component';
import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component';
import { FilterTextComponent } from './filter/filter-text.component';
import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component';
import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component';
import { DeviceProfileProvisionConfigurationComponent } from "./profile/device-profile-provision-configuration.component";
import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component';
import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component';
import { DeviceCredentialsComponent } from './device/device-credentials.component';
import { AlarmScheduleInfoComponent } from './profile/alarm/alarm-schedule-info.component';
import { FilterUserInfoComponent } from '@home/components/filter/filter-user-info.component';
import { FilterUserInfoDialogComponent } from '@home/components/filter/filter-user-info-dialog.component';
import { FilterPredicateValueComponent } from '@home/components/filter/filter-predicate-value.component';
import { TenantProfileAutocompleteComponent } from '@home/components/profile/tenant-profile-autocomplete.component';
import { TenantProfileComponent } from '@home/components/profile/tenant-profile.component';
import { TenantProfileDialogComponent } from '@home/components/profile/tenant-profile-dialog.component';
import { TenantProfileDataComponent } from '@home/components/profile/tenant-profile-data.component';
import { DefaultDeviceProfileConfigurationComponent } from '@home/components/profile/device/default-device-profile-configuration.component';
import { DeviceProfileConfigurationComponent } from '@home/components/profile/device/device-profile-configuration.component';
import { DeviceProfileComponent } from '@home/components/profile/device-profile.component';
import { DefaultDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/default-device-profile-transport-configuration.component';
import { DeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/device-profile-transport-configuration.component';
import { DeviceProfileDialogComponent } from '@home/components/profile/device-profile-dialog.component';
import { DeviceProfileAutocompleteComponent } from '@home/components/profile/device-profile-autocomplete.component';
import { MqttDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/mqtt-device-profile-transport-configuration.component';
import { Lwm2mDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/lwm2m-device-profile-transport-configuration.component';
import { DeviceProfileAlarmsComponent } from '@home/components/profile/alarm/device-profile-alarms.component';
import { DeviceProfileAlarmComponent } from '@home/components/profile/alarm/device-profile-alarm.component';
import { CreateAlarmRulesComponent } from '@home/components/profile/alarm/create-alarm-rules.component';
import { AlarmRuleComponent } from '@home/components/profile/alarm/alarm-rule.component';
import { AlarmRuleConditionComponent } from '@home/components/profile/alarm/alarm-rule-condition.component';
import { FilterTextComponent } from '@home/components/filter/filter-text.component';
import { AddDeviceProfileDialogComponent } from '@home/components/profile/add-device-profile-dialog.component';
import { RuleChainAutocompleteComponent } from '@home/components/rule-chain/rule-chain-autocomplete.component';
import { DeviceProfileProvisionConfigurationComponent } from '@home/components/profile/device-profile-provision-configuration.component';
import { AlarmScheduleComponent } from '@home/components/profile/alarm/alarm-schedule.component';
import { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component';
import { DeviceCredentialsComponent } from '@home/components/device/device-credentials.component';
import { AlarmScheduleInfoComponent } from '@home/components/profile/alarm/alarm-schedule-info.component';
import { AlarmScheduleDialogComponent } from '@home/components/profile/alarm/alarm-schedule-dialog.component';
import { EditAlarmDetailsDialogComponent } from './profile/alarm/edit-alarm-details-dialog.component';
import { EditAlarmDetailsDialogComponent } from '@home/components/profile/alarm/edit-alarm-details-dialog.component';
import { AlarmRuleConditionDialogComponent } from '@home/components/profile/alarm/alarm-rule-condition-dialog.component';
import { DefaultTenantProfileConfigurationComponent } from './profile/tenant/default-tenant-profile-configuration.component';
import { TenantProfileConfigurationComponent } from './profile/tenant/tenant-profile-configuration.component';
import { DefaultTenantProfileConfigurationComponent } from '@home/components/profile/tenant/default-tenant-profile-configuration.component';
import { TenantProfileConfigurationComponent } from '@home/components/profile/tenant/tenant-profile-configuration.component';
import { SmsProviderConfigurationComponent } from '@home/components/sms/sms-provider-configuration.component';
import { AwsSnsProviderConfigurationComponent } from '@home/components/sms/aws-sns-provider-configuration.component';
import { TwilioSmsProviderConfigurationComponent } from '@home/components/sms/twilio-sms-provider-configuration.component';
@NgModule({
declarations:
@ -212,7 +215,10 @@ import { TenantProfileConfigurationComponent } from './profile/tenant/tenant-pro
DeviceWizardDialogComponent,
DeviceCredentialsComponent,
AlarmScheduleDialogComponent,
EditAlarmDetailsDialogComponent
EditAlarmDetailsDialogComponent,
SmsProviderConfigurationComponent,
AwsSnsProviderConfigurationComponent,
TwilioSmsProviderConfigurationComponent
],
imports: [
CommonModule,
@ -298,7 +304,9 @@ import { TenantProfileConfigurationComponent } from './profile/tenant/tenant-pro
AlarmScheduleDialogComponent,
EditAlarmDetailsDialogComponent,
DeviceProfileProvisionConfigurationComponent,
AlarmScheduleComponent
SmsProviderConfigurationComponent,
AwsSnsProviderConfigurationComponent,
TwilioSmsProviderConfigurationComponent
],
providers: [
WidgetComponentService,

40
ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html

@ -0,0 +1,40 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="awsSnsProviderConfigurationFormGroup" style="padding-bottom: 16px;">
<mat-form-field class="mat-block">
<mat-label translate>admin.aws-access-key-id</mat-label>
<input required matInput formControlName="accessKeyId">
<mat-error *ngIf="awsSnsProviderConfigurationFormGroup.get('accessKeyId').hasError('required')">
{{ 'admin.aws-access-key-id-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.aws-secret-access-key</mat-label>
<input required type="password" matInput formControlName="secretAccessKey">
<mat-error *ngIf="awsSnsProviderConfigurationFormGroup.get('secretAccessKey').hasError('required')">
{{ 'admin.aws-secret-access-key-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.aws-region</mat-label>
<input required matInput formControlName="region">
<mat-error *ngIf="awsSnsProviderConfigurationFormGroup.get('region').hasError('required')">
{{ 'admin.aws-region-required' | translate }}
</mat-error>
</mat-form-field>
</form>

99
ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.ts

@ -0,0 +1,99 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { isDefinedAndNotNull } from '@core/utils';
import { AwsSnsSmsProviderConfiguration } from '@shared/models/settings.models';
@Component({
selector: 'tb-aws-sns-provider-configuration',
templateUrl: './aws-sns-provider-configuration.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AwsSnsProviderConfigurationComponent),
multi: true
}]
})
export class AwsSnsProviderConfigurationComponent implements ControlValueAccessor, OnInit {
awsSnsProviderConfigurationFormGroup: FormGroup;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private fb: FormBuilder) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.awsSnsProviderConfigurationFormGroup = this.fb.group({
accessKeyId: [null, [Validators.required]],
secretAccessKey: [null, [Validators.required]],
region: [null, [Validators.required]]
});
this.awsSnsProviderConfigurationFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.awsSnsProviderConfigurationFormGroup.disable({emitEvent: false});
} else {
this.awsSnsProviderConfigurationFormGroup.enable({emitEvent: false});
}
}
writeValue(value: AwsSnsSmsProviderConfiguration | null): void {
if (isDefinedAndNotNull(value)) {
this.awsSnsProviderConfigurationFormGroup.patchValue(value, {emitEvent: false});
}
}
private updateModel() {
let configuration: AwsSnsSmsProviderConfiguration = null;
if (this.awsSnsProviderConfigurationFormGroup.valid) {
configuration = this.awsSnsProviderConfigurationFormGroup.value;
}
this.propagateChange(configuration);
}
}

44
ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html

@ -0,0 +1,44 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="smsProviderConfigurationFormGroup">
<mat-form-field class="mat-block">
<mat-label translate>admin.sms-provider-type</mat-label>
<mat-select formControlName="type" [required]="required">
<mat-option *ngFor="let type of smsProviderTypes" [value]="type">
{{smsProviderTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-error *ngIf="smsProviderConfigurationFormGroup.get('type').hasError('required')">
{{ 'admin.sms-provider-type-required' | translate }}
</mat-error>
</mat-form-field>
<div [ngSwitch]="smsProviderConfigurationFormGroup.get('type').value">
<ng-template [ngSwitchCase]="smsProviderType.AWS_SNS">
<tb-aws-sns-provider-configuration
[required]="required"
formControlName="configuration">
</tb-aws-sns-provider-configuration>
</ng-template>
<ng-template [ngSwitchCase]="smsProviderType.TWILIO">
<tb-twilio-sms-provider-configuration
[required]="required"
formControlName="configuration">
</tb-twilio-sms-provider-configuration>
</ng-template>
</div>
</div>

123
ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.ts

@ -0,0 +1,123 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
DeviceProfileTransportConfiguration,
DeviceTransportType,
deviceTransportTypeTranslationMap
} from '@shared/models/device.models';
import { deepClone } from '@core/utils';
import {
createSmsProviderConfiguration,
SmsProviderConfiguration,
SmsProviderType,
smsProviderTypeTranslationMap
} from '@shared/models/settings.models';
@Component({
selector: 'tb-sms-provider-configuration',
templateUrl: './sms-provider-configuration.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SmsProviderConfigurationComponent),
multi: true
}]
})
export class SmsProviderConfigurationComponent implements ControlValueAccessor, OnInit {
smsProviderType = SmsProviderType;
smsProviderTypes = Object.keys(SmsProviderType);
smsProviderTypeTranslations = smsProviderTypeTranslationMap;
smsProviderConfigurationFormGroup: FormGroup;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private fb: FormBuilder) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.smsProviderConfigurationFormGroup = this.fb.group({
type: [null, Validators.required],
configuration: [null, Validators.required]
});
this.smsProviderConfigurationFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
this.smsProviderConfigurationFormGroup.get('type').valueChanges.subscribe(() => {
this.smsProviderTypeChanged();
});
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.smsProviderConfigurationFormGroup.disable({emitEvent: false});
} else {
this.smsProviderConfigurationFormGroup.enable({emitEvent: false});
}
}
writeValue(value: SmsProviderConfiguration | null): void {
const configuration = deepClone(value);
const type = configuration?.type;
if (configuration) {
delete configuration.type;
}
this.smsProviderConfigurationFormGroup.patchValue({type}, {emitEvent: false});
this.smsProviderConfigurationFormGroup.patchValue({configuration}, {emitEvent: false});
}
private smsProviderTypeChanged() {
const type: SmsProviderType = this.smsProviderConfigurationFormGroup.get('type').value;
this.smsProviderConfigurationFormGroup.patchValue({configuration: createSmsProviderConfiguration(type)}, {emitEvent: false});
}
private updateModel() {
let configuration: SmsProviderConfiguration = null;
if (this.smsProviderConfigurationFormGroup.valid) {
configuration = this.smsProviderConfigurationFormGroup.getRawValue().configuration;
configuration.type = this.smsProviderConfigurationFormGroup.getRawValue().type;
}
this.propagateChange(configuration);
}
}

44
ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.html

@ -0,0 +1,44 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="twilioSmsProviderConfigurationFormGroup" style="padding-bottom: 16px;">
<mat-form-field class="mat-block">
<mat-label translate>admin.number-from</mat-label>
<input type="tel" required [pattern]="phoneNumberPattern" matInput formControlName="numberFrom">
<mat-error *ngIf="twilioSmsProviderConfigurationFormGroup.get('numberFrom').hasError('required')">
{{ 'admin.number-from-required' | translate }}
</mat-error>
<mat-error *ngIf="twilioSmsProviderConfigurationFormGroup.get('numberFrom').hasError('pattern')">
{{ 'admin.phone-number-pattern' | translate }}
</mat-error>
<mat-hint innerHTML="{{ 'admin.phone-number-hint' | translate }}"></mat-hint>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.twilio-account-sid</mat-label>
<input required matInput formControlName="accountSid">
<mat-error *ngIf="twilioSmsProviderConfigurationFormGroup.get('accountSid').hasError('required')">
{{ 'admin.twilio-account-sid-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.twilio-account-token</mat-label>
<input required type="password" matInput formControlName="accountToken">
<mat-error *ngIf="twilioSmsProviderConfigurationFormGroup.get('accountToken').hasError('required')">
{{ 'admin.twilio-account-token-required' | translate }}
</mat-error>
</mat-form-field>
</form>

101
ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.ts

@ -0,0 +1,101 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { isDefinedAndNotNull } from '@core/utils';
import { phoneNumberPattern, TwilioSmsProviderConfiguration } from '@shared/models/settings.models';
@Component({
selector: 'tb-twilio-sms-provider-configuration',
templateUrl: './twilio-sms-provider-configuration.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TwilioSmsProviderConfigurationComponent),
multi: true
}]
})
export class TwilioSmsProviderConfigurationComponent implements ControlValueAccessor, OnInit {
twilioSmsProviderConfigurationFormGroup: FormGroup;
phoneNumberPattern = phoneNumberPattern;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private fb: FormBuilder) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.twilioSmsProviderConfigurationFormGroup = this.fb.group({
numberFrom: [null, [Validators.required, Validators.pattern(phoneNumberPattern)]],
accountSid: [null, [Validators.required]],
accountToken: [null, [Validators.required]]
});
this.twilioSmsProviderConfigurationFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.twilioSmsProviderConfigurationFormGroup.disable({emitEvent: false});
} else {
this.twilioSmsProviderConfigurationFormGroup.enable({emitEvent: false});
}
}
writeValue(value: TwilioSmsProviderConfiguration | null): void {
if (isDefinedAndNotNull(value)) {
this.twilioSmsProviderConfigurationFormGroup.patchValue(value, {emitEvent: false});
}
}
private updateModel() {
let configuration: TwilioSmsProviderConfiguration = null;
if (this.twilioSmsProviderConfigurationFormGroup.valid) {
configuration = this.twilioSmsProviderConfigurationFormGroup.value;
}
this.propagateChange(configuration);
}
}

14
ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts

@ -31,6 +31,7 @@ import { Observable } from 'rxjs';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { OAuth2Service } from '@core/http/oauth2.service';
import { UserProfileResolver } from '@home/pages/profile/profile-routing.module';
import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
@Injectable()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@ -85,6 +86,19 @@ const routes: Routes = [
}
}
},
{
path: 'sms-provider',
component: SmsProviderComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.sms-provider-settings',
breadcrumb: {
label: 'admin.sms-provider',
icon: 'sms'
}
}
},
{
path: 'security-settings',
component: SecuritySettingsComponent,

4
ui-ngx/src/app/modules/home/pages/admin/admin.module.ts

@ -24,12 +24,16 @@ import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-sett
import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component';
import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component';
@NgModule({
declarations:
[
GeneralSettingsComponent,
MailServerComponent,
SmsProviderComponent,
SendTestSmsDialogComponent,
SecuritySettingsComponent,
OAuth2SettingsComponent
],

70
ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.html

@ -0,0 +1,70 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="sendTestSmsFormGroup" style="min-width: 500px; position: relative;">
<mat-toolbar color="primary">
<h2 translate>admin.send-test-sms</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content tb-toast toastTarget="sendTestSmsDialogContent">
<fieldset [disabled]="(isLoading$ | async)">
<mat-form-field class="mat-block">
<mat-label translate>admin.number-to</mat-label>
<input type="tel" required [pattern]="phoneNumberPattern" matInput formControlName="numberTo">
<mat-error *ngIf="sendTestSmsFormGroup.get('numberTo').hasError('required')">
{{ 'admin.number-to-required' | translate }}
</mat-error>
<mat-error *ngIf="sendTestSmsFormGroup.get('numberTo').hasError('pattern')">
{{ 'admin.phone-number-pattern' | translate }}
</mat-error>
<mat-hint innerHTML="{{ 'admin.phone-number-hint' | translate }}"></mat-hint>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.sms-message</mat-label>
<textarea required matInput rows="3" [maxLength]="1600" formControlName="message"></textarea>
<mat-error *ngIf="sendTestSmsFormGroup.get('message').hasError('required')">
{{ 'admin.sms-message-required' | translate }}
</mat-error>
<mat-error *ngIf="sendTestSmsFormGroup.get('message').hasError('maxLength')">
{{ 'admin.sms-message-max-length' | translate }}
</mat-error>
</mat-form-field>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="close()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="sendTestSms()"
[disabled]="(isLoading$ | async) || sendTestSmsFormGroup.invalid">
{{ 'admin.send-test-sms' | translate }}
</button>
</div>
</form>

87
ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.ts

@ -0,0 +1,87 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { phoneNumberPattern, SmsProviderConfiguration, TestSmsRequest } from '@shared/models/settings.models';
import { AdminService } from '@core/http/admin.service';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
export interface SendTestSmsDialogData {
smsProviderConfiguration: SmsProviderConfiguration;
}
@Component({
selector: 'tb-send-test-sms-dialog',
templateUrl: './send-test-sms-dialog.component.html',
styleUrls: []
})
export class SendTestSmsDialogComponent extends
DialogComponent<SendTestSmsDialogComponent> implements OnInit {
phoneNumberPattern = phoneNumberPattern;
sendTestSmsFormGroup: FormGroup;
smsProviderConfiguration = this.data.smsProviderConfiguration;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: SendTestSmsDialogData,
private adminService: AdminService,
private translate: TranslateService,
public dialogRef: MatDialogRef<SendTestSmsDialogComponent>,
public fb: FormBuilder) {
super(store, router, dialogRef);
}
ngOnInit(): void {
this.sendTestSmsFormGroup = this.fb.group({
numberTo: [null, [Validators.required, Validators.pattern(phoneNumberPattern)]],
message: [null, [Validators.required, Validators.maxLength(1600)]]
});
}
close(): void {
this.dialogRef.close();
}
sendTestSms(): void {
const request: TestSmsRequest = {
providerConfiguration: this.smsProviderConfiguration,
numberTo: this.sendTestSmsFormGroup.value.numberTo,
message: this.sendTestSmsFormGroup.value.message
};
this.adminService.sendTestSms(request).subscribe(
() => {
this.store.dispatch(new ActionNotificationShow(
{
message: this.translate.instant('admin.test-sms-sent'),
target: 'sendTestSmsDialogContent',
verticalPosition: 'bottom',
horizontalPosition: 'left',
type: 'success'
}));
}
);
}
}

50
ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html

@ -0,0 +1,50 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div>
<mat-card class="settings-card">
<mat-card-title>
<div fxLayout="row">
<span class="mat-headline" translate>admin.sms-provider-settings</span>
<span fxFlex></span>
<div tb-help="smsProviderSettings"></div>
</div>
</mat-card-title>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<mat-card-content style="padding-top: 16px;">
<form [formGroup]="smsProvider" (ngSubmit)="save()">
<fieldset [disabled]="isLoading$ | async">
<tb-sms-provider-configuration
required
formControlName="configuration">
</tb-sms-provider-configuration>
<div fxLayout="row" fxLayoutAlign="end center" fxLayout.xs="column" fxLayoutAlign.xs="end" fxLayoutGap="16px">
<button mat-raised-button type="button"
[disabled]="(isLoading$ | async) || smsProvider.invalid" (click)="sendTestSms()">
{{'admin.send-test-sms' | translate}}
</button>
<button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || smsProvider.invalid || !smsProvider.dirty"
type="submit">{{'action.save' | translate}}
</button>
</div>
</fieldset>
</form>
</mat-card-content>
</mat-card>
</div>

18
ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.scss

@ -0,0 +1,18 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
}

95
ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.ts

@ -0,0 +1,95 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AdminSettings, SmsProviderConfiguration } from '@shared/models/settings.models';
import { AdminService } from '@core/http/admin.service';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { MatDialog } from '@angular/material/dialog';
import { SendTestSmsDialogComponent, SendTestSmsDialogData } from '@home/pages/admin/send-test-sms-dialog.component';
@Component({
selector: 'tb-sms-provider',
templateUrl: './sms-provider.component.html',
styleUrls: ['./sms-provider.component.scss', './settings-card.scss']
})
export class SmsProviderComponent extends PageComponent implements OnInit, HasConfirmForm {
smsProvider: FormGroup;
adminSettings: AdminSettings<SmsProviderConfiguration>;
constructor(protected store: Store<AppState>,
private router: Router,
private adminService: AdminService,
private dialog: MatDialog,
public fb: FormBuilder) {
super(store);
}
ngOnInit() {
this.buildSmsProviderForm();
this.adminService.getAdminSettings<SmsProviderConfiguration>('sms', {ignoreErrors: true}).subscribe(
(adminSettings) => {
this.adminSettings = adminSettings;
this.smsProvider.reset({configuration: this.adminSettings.jsonValue});
},
() => {
this.adminSettings = {
key: 'sms',
jsonValue: null
};
this.smsProvider.reset({configuration: this.adminSettings.jsonValue});
}
);
}
buildSmsProviderForm() {
this.smsProvider = this.fb.group({
configuration: [null, [Validators.required]]
});
this.registerDisableOnLoadFormControl(this.smsProvider.get('configuration'));
}
sendTestSms(): void {
this.dialog.open<SendTestSmsDialogComponent, SendTestSmsDialogData>(SendTestSmsDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
smsProviderConfiguration: this.smsProvider.value.configuration
}
});
}
save(): void {
this.adminSettings.jsonValue = this.smsProvider.value.configuration;
this.adminService.saveAdminSettings(this.adminSettings).subscribe(
(adminSettings) => {
this.adminSettings = adminSettings;
this.smsProvider.reset({configuration: this.adminSettings.jsonValue});
}
);
}
confirmForm(): FormGroup {
return this.smsProvider;
}
}

2
ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div style="min-width: 400px;">
<div style="min-width: 400px; position: relative;">
<mat-toolbar fxLayout="row" color="primary">
<h2>{{ 'dashboard.public-dashboard-title' | translate }}</h2>
<span fxFlex></span>

2
ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<form style="min-width: 400px;">
<form style="min-width: 400px; position: relative;">
<mat-toolbar color="primary">
<h2 translate>user.activation-link</h2>
<span fxFlex></span>

1
ui-ngx/src/app/shared/models/constants.ts

@ -59,6 +59,7 @@ const helpBaseUrl = 'https://thingsboard.io';
export const HelpLinks = {
linksMap: {
outgoingMailSettings: helpBaseUrl + '/docs/user-guide/ui/mail-settings',
smsProviderSettings: helpBaseUrl + '/docs/user-guide/ui/sms-provider-settings',
securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings',
oauth2Settings: helpBaseUrl + '/docs/user-guide/oauth-2-support/',
ruleEngine: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/overview/',

65
ui-ngx/src/app/shared/models/settings.models.ts

@ -14,6 +14,8 @@
/// limitations under the License.
///
import { DeviceTransportType } from '@shared/models/device.models';
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])$/;
export interface AdminSettings<T> {
@ -60,3 +62,66 @@ export interface UpdateMessage {
message: string;
updateAvailable: boolean;
}
export const phoneNumberPattern = /^\+[1-9]\d{1,14}$/;
export enum SmsProviderType {
AWS_SNS = 'AWS_SNS',
TWILIO = 'TWILIO'
}
export const smsProviderTypeTranslationMap = new Map<SmsProviderType, string>(
[
[SmsProviderType.AWS_SNS, 'admin.sms-provider-type-aws-sns'],
[SmsProviderType.TWILIO, 'admin.sms-provider-type-twilio']
]
);
export interface AwsSnsSmsProviderConfiguration {
accessKeyId?: string;
secretAccessKey?: string;
region?: string;
}
export interface TwilioSmsProviderConfiguration {
accountSid?: string;
accountToken?: string;
numberFrom?: string;
}
export type SmsProviderConfigurations = AwsSnsSmsProviderConfiguration & TwilioSmsProviderConfiguration;
export interface SmsProviderConfiguration extends SmsProviderConfigurations {
type: SmsProviderType;
}
export interface TestSmsRequest {
providerConfiguration: SmsProviderConfiguration;
numberTo: string;
message: string;
}
export function createSmsProviderConfiguration(type: SmsProviderType): SmsProviderConfiguration {
let smsProviderConfiguration: SmsProviderConfiguration;
if (type) {
switch (type) {
case SmsProviderType.AWS_SNS:
const awsSnsSmsProviderConfiguration: AwsSnsSmsProviderConfiguration = {
accessKeyId: '',
secretAccessKey: '',
region: 'us-east-1'
};
smsProviderConfiguration = {...awsSnsSmsProviderConfiguration, type: SmsProviderType.AWS_SNS};
break;
case SmsProviderType.TWILIO:
const twilioSmsProviderConfiguration: TwilioSmsProviderConfiguration = {
numberFrom: '',
accountSid: '',
accountToken: ''
};
smsProviderConfiguration = {...twilioSmsProviderConfiguration, type: SmsProviderType.TWILIO};
break;
}
}
return smsProviderConfiguration;
}

27
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -104,6 +104,33 @@
"proxy-user": "Proxy user",
"proxy-password": "Proxy password",
"send-test-mail": "Send test mail",
"sms-provider": "SMS provider",
"sms-provider-settings": "SMS provider settings",
"sms-provider-type": "SMS provider type",
"sms-provider-type-required": "SMS provider type is required.",
"sms-provider-type-aws-sns": "Amazon SNS",
"sms-provider-type-twilio": "Twilio",
"aws-access-key-id": "AWS Access Key ID",
"aws-access-key-id-required": "AWS Access Key ID is required",
"aws-secret-access-key": "AWS Secret Access Key",
"aws-secret-access-key-required": "AWS Secret Access Key is required",
"aws-region": "AWS Region",
"aws-region-required": "AWS Region is required",
"number-from": "Phone Number From",
"number-from-required": "Phone Number From is required.",
"number-to": "Phone Number To",
"number-to-required": "Phone Number To is required.",
"phone-number-hint": "Phone Number in E.164 format, ex. +19995550123",
"phone-number-pattern": "Invalid phone number. Should be in E.164 format, ex. +19995550123.",
"sms-message": "SMS message",
"sms-message-required": "SMS message is required.",
"sms-message-max-length": "SMS message can't be longer 1600 characters",
"twilio-account-sid": "Twilio Account SID",
"twilio-account-sid-required": "Twilio Account SID is required",
"twilio-account-token": "Twilio Account Token",
"twilio-account-token-required": "Twilio Account Token is required",
"send-test-sms": "Send test SMS",
"test-sms-sent": "Test SMS was successfully sent!",
"security-settings": "Security settings",
"password-policy": "Password policy",
"minimum-password-length": "Minimum password length",

Loading…
Cancel
Save