Browse Source

Merge branch 'master' into improvement/provisioning-gateway-device

pull/11341/head
Illia Barkov 2 years ago
committed by GitHub
parent
commit
4fb982eee8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 70
      application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg
  2. 72
      application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg
  3. 7
      application/src/main/data/json/system/scada_symbols/sand-filter.svg
  4. 4
      application/src/main/data/json/system/scada_symbols/vertical-ball-valve.svg
  5. 74
      application/src/main/data/json/system/widget_bundles/scada_fluid_system.json
  6. 2
      application/src/main/data/json/system/widget_bundles/scada_symbols.json
  7. 74
      application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json
  8. 2
      application/src/main/data/json/system/widget_types/scada_symbol.json
  9. 19
      application/src/main/data/upgrade/3.7.0/schema_update.sql
  10. 6
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  11. 126
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  12. 25
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  13. 2
      application/src/main/java/org/thingsboard/server/controller/ImageController.java
  14. 3
      application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java
  15. 1
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  16. 69
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  17. 13
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  18. 2
      application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java
  19. 32
      application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java
  20. 5
      application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java
  21. 1
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
  22. 11
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  23. 2
      application/src/main/java/org/thingsboard/server/service/mail/DefaultTbMailConfigTemplateService.java
  24. 10
      application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java
  25. 8
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  26. 82
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  27. 6
      application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java
  28. 10
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  29. 2
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  30. 2
      application/src/main/resources/templates/activation.ftl
  31. 2
      application/src/main/resources/templates/reset.password.ftl
  32. 2
      application/src/main/resources/thingsboard.yml
  33. 5
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  34. 184
      application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java
  35. 25
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  36. 18
      application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
  37. 12
      application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java
  38. 5
      common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java
  39. 2
      common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java
  40. 16
      common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java
  41. 8
      common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java
  42. 26
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedCacheKey.java
  43. 2
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java
  44. 19
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java
  45. 2
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbCache.java
  46. 9
      common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java
  47. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  48. 19
      common/data/src/main/java/org/thingsboard/server/common/data/UserActivationLink.java
  49. 104
      common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java
  50. 25
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java
  51. 32
      common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplate.java
  52. 2
      common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqSettings.java
  53. 19
      common/queue/src/main/java/org/thingsboard/server/queue/scheduler/DefaultSchedulerComponent.java
  54. 128
      common/queue/src/test/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplateTest.java
  55. 95
      common/queue/src/test/java/org/thingsboard/server/queue/scheduler/DefaultSchedulerComponentTest.java
  56. 4
      common/transport/coap/src/main/proto/efento/proto_config.proto
  57. 1
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  58. 97
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/AbstractActivityManager.java
  59. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/FirstAndLastEventActivityStrategy.java
  60. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/FirstEventActivityStrategy.java
  61. 10
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  62. 9
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCacheKey.java
  63. 7
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java
  64. 9
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCacheKey.java
  65. 9
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java
  66. 7
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
  67. 4
      dao/src/main/java/org/thingsboard/server/dao/edge/BaseRelatedEdgesService.java
  68. 3
      dao/src/main/java/org/thingsboard/server/dao/entity/CachedVersionedEntityService.java
  69. 9
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java
  70. 2
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  71. 10
      dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java
  72. 6
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  73. 81
      dao/src/main/java/org/thingsboard/server/dao/settings/DefaultSecuritySettingsService.java
  74. 26
      dao/src/main/java/org/thingsboard/server/dao/settings/SecuritySettingsService.java
  75. 9
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestCacheKey.java
  76. 29
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  77. 2
      dao/src/main/resources/sql/schema-entities.sql
  78. 12
      dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java
  79. 12
      dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java
  80. 2
      dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserCredentialsDaoTest.java
  81. 2
      msa/js-executor/config/default.yml
  82. 26
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java
  83. 9
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  84. 6
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java
  85. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
  86. 50
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  87. 9
      ui-ngx/src/app/core/api/alias-controller.ts
  88. 1
      ui-ngx/src/app/core/api/widget-api.models.ts
  89. 2
      ui-ngx/src/app/core/http/ota-package.service.ts
  90. 6
      ui-ngx/src/app/core/http/user.service.ts
  91. 12
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  92. 100
      ui-ngx/src/app/core/services/dynamic-component-factory.service.ts
  93. 71
      ui-ngx/src/app/core/services/resources.service.ts
  94. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  95. 85
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html
  96. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html
  97. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts
  98. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html
  99. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts
  100. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html

70
application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 56 KiB

72
application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 56 KiB

7
application/src/main/data/json/system/scada_symbols/stand-filter.svg → application/src/main/data/json/system/scada_symbols/sand-filter.svg

@ -1,9 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="600" height="1e3" fill="none" version="1.1" viewBox="0 0 600 1e3">
<tb:metadata xmlns=""><![CDATA[{
"title": "Stand filter",
"description": "Stand filter with configurable filtration mode option and various states.",
"title": "Sand filter",
"description": "Sand filter with configurable filtration mode option and various states.",
"searchTags": [
"filter"
"filter",
"sand"
],
"widgetSizeX": 3,
"widgetSizeY": 5,

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

4
application/src/main/data/json/system/scada_symbols/vertical-ball-valve.svg

@ -93,7 +93,7 @@
},
"valueToData": {
"type": "CONSTANT",
"constantValue": false,
"constantValue": true,
"valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
}
},
@ -127,7 +127,7 @@
},
"valueToData": {
"type": "CONSTANT",
"constantValue": true,
"constantValue": false,
"valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
}
},

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

74
application/src/main/data/json/system/widget_bundles/scada_fluid_system.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_bundles/scada_symbols.json

File diff suppressed because one or more lines are too long

74
application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json

@ -1,74 +0,0 @@
{
"widgetsBundle": {
"alias": "scada_water_system_symbols",
"title": "SCADA water system symbols",
"scada": true,
"image": null,
"description": "Bundle with SCADA symbols for water system",
"order": 9300,
"name": "SCADA water system symbols"
},
"widgetTypeFqns": [
"horizontal_pipe",
"long_horizontal_pipe",
"vertical_pipe",
"long_vertical_pipe",
"left_bottom_elbow_pipe",
"bottom_right_elbow_pipe",
"top_right_elbow_pipe",
"left_top_elbow_pipe",
"cross_pipe",
"left_tee_pipe",
"bottom_tee_pipe",
"right_tee_pipe",
"top_tee_pipe",
"right_elbow_drain_pipe",
"left_elbow_drain_pipe",
"left_drain_pipe",
"right_drain_pipe",
"short_left_drain_pipe",
"short_right_drain_pipe",
"top_flow_meter",
"right_flow_meter",
"bottom_flow_meter",
"left_flow_meter",
"horizontal_inline_flow_meter",
"vertical_inline_flow_meter",
"left_analog_water_level_meter",
"right_analog_water_level_meter",
"leak_sensor",
"centrifugal_pump",
"small_right_motor_pump",
"small_left_motor_pump",
"right_motor_pump",
"left_motor_pump",
"right_heat_pump",
"left_heat_pump",
"short_bottom_filter",
"long_bottom_filter",
"short_top_filter",
"long_top_filter",
"stand_filter",
"horizontal_wheel_valve",
"vertical_wheel_valve",
"horizontal_ball_valve",
"vertical_ball_valve",
"water_stop",
"vertical_tank",
"stand_vertical_tank",
"cylindrical_tank",
"stand_cylindrical_tank",
"vertical_short_tank",
"stand_vertical_short_tank",
"large_cylindrical_tank",
"large_stand_cylindrical_tank",
"large_vertical_tank",
"large_stand_vertical_tank",
"horizontal_tank",
"stand_horizontal_tank",
"spherical_tank",
"small_spherical_tank",
"elevated_tank",
"pool"
]
}

2
application/src/main/data/json/system/widget_types/scada_symbol.json

File diff suppressed because one or more lines are too long

19
application/src/main/data/upgrade/3.7.0/schema_update.sql

@ -195,4 +195,21 @@ $$
END
$$;
-- OAUTH2 UPDATE END
-- OAUTH2 UPDATE END
-- USER CREDENTIALS UPDATE START
ALTER TABLE user_credentials ADD COLUMN IF NOT EXISTS activate_token_exp_time BIGINT;
-- Setting 24-hour TTL for existing activation tokens
UPDATE user_credentials SET activate_token_exp_time = cast(extract(EPOCH FROM NOW()) * 1000 AS BIGINT) + 86400000
WHERE activate_token IS NOT NULL AND activate_token_exp_time IS NULL;
ALTER TABLE user_credentials ADD COLUMN IF NOT EXISTS reset_token_exp_time BIGINT;
-- Setting 24-hour TTL for existing password reset tokens
UPDATE user_credentials SET reset_token_exp_time = cast(extract(EPOCH FROM NOW()) * 1000 AS BIGINT) + 86400000
WHERE reset_token IS NOT NULL AND reset_token_exp_time IS NULL;
UPDATE admin_settings SET json_value = (json_value::jsonb || '{"userActivationTokenTtl":24,"passwordResetTokenTtl":24}'::jsonb)::varchar
WHERE key = 'securitySettings';
-- USER CREDENTIALS UPDATE END

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

@ -73,6 +73,7 @@ import org.thingsboard.server.common.data.sync.vc.VcUtils;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.service.security.auth.oauth2.CookieUtils;
@ -109,6 +110,7 @@ public class AdminController extends BaseController {
private final SmsService smsService;
private final AdminSettingsService adminSettingsService;
private final SystemSecurityService systemSecurityService;
private final SecuritySettingsService securitySettingsService;
private final JwtSettingsService jwtSettingsService;
private final JwtTokenFactory tokenFactory;
private final EntitiesVersionControlService versionControlService;
@ -167,7 +169,7 @@ public class AdminController extends BaseController {
@ResponseBody
public SecuritySettings getSecuritySettings() throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return checkNotNull(systemSecurityService.getSecuritySettings());
return checkNotNull(securitySettingsService.getSecuritySettings());
}
@ApiOperation(value = "Update Security Settings (saveSecuritySettings)",
@ -179,7 +181,7 @@ public class AdminController extends BaseController {
@Parameter(description = "A JSON value representing the Security Settings.")
@RequestBody SecuritySettings securitySettings) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
securitySettings = checkNotNull(systemSecurityService.saveSecuritySettings(securitySettings));
securitySettings = checkNotNull(securitySettingsService.saveSecuritySettings(securitySettings));
return securitySettings;
}

126
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -21,17 +21,15 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.cache.limits.RateLimitService;
@ -48,6 +46,7 @@ import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.ActivateUserRequest;
@ -59,9 +58,6 @@ import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.net.URI;
import java.net.URISyntaxException;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@ -75,6 +71,7 @@ public class AuthController extends BaseController {
private final JwtTokenFactory tokenFactory;
private final MailService mailService;
private final SystemSecurityService systemSecurityService;
private final SecuritySettingsService securitySettingsService;
private final RateLimitService rateLimitService;
private final ApplicationEventPublisher eventPublisher;
@ -82,9 +79,8 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Get current User (getUser)",
notes = "Get the information about the User which credentials are used to perform this REST API call.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/auth/user", method = RequestMethod.GET)
public @ResponseBody
User getUser() throws ThingsboardException {
@GetMapping(value = "/auth/user")
public User getUser() throws ThingsboardException {
SecurityUser securityUser = getCurrentUser();
return userService.findUserById(securityUser.getTenantId(), securityUser.getId());
}
@ -92,8 +88,7 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Logout (logout)",
notes = "Special API call to record the 'logout' of the user to the Audit Logs. Since platform uses [JWT](https://jwt.io/), the actual logout is the procedure of clearing the [JWT](https://jwt.io/) token on the client side. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/auth/logout", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@PostMapping(value = "/auth/logout")
public void logout(HttpServletRequest request) throws ThingsboardException {
logLogoutAction(request);
}
@ -101,8 +96,7 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Change password for current User (changePassword)",
notes = "Change the password for the User which credentials are used to perform this REST API call. Be aware that previously generated [JWT](https://jwt.io/) tokens will be still valid until they expire.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@PostMapping(value = "/auth/changePassword")
public JwtPair changePassword(@Parameter(description = "Change Password Request")
@RequestBody ChangePasswordRequest changePasswordRequest) throws ThingsboardException {
String currentPassword = changePasswordRequest.getCurrentPassword();
@ -125,46 +119,34 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Get the current User password policy (getUserPasswordPolicy)",
notes = "API call to get the password policy for the password validation form(s).")
@RequestMapping(value = "/noauth/userPasswordPolicy", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/noauth/userPasswordPolicy")
public UserPasswordPolicy getUserPasswordPolicy() throws ThingsboardException {
SecuritySettings securitySettings =
checkNotNull(systemSecurityService.getSecuritySettings());
SecuritySettings securitySettings = checkNotNull(securitySettingsService.getSecuritySettings());
return securitySettings.getPasswordPolicy();
}
@ApiOperation(value = "Check Activate User Token (checkActivateToken)",
notes = "Checks the activation token and forwards user to 'Create Password' page. " +
"If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Create Password' page and same 'activateToken' specified in the URL parameters. " +
"If token is not valid, returns '409 Conflict'.")
@RequestMapping(value = "/noauth/activate", params = {"activateToken"}, method = RequestMethod.GET)
public ResponseEntity<String> checkActivateToken(
"If token is not valid, returns '409 Conflict'. " +
"If token is expired, redirects to error page.")
@GetMapping(value = "/noauth/activate", params = {"activateToken"})
public ResponseEntity<?> checkActivateToken(
@Parameter(description = "The activate token string.")
@RequestParam(value = "activateToken") String activateToken) {
HttpHeaders headers = new HttpHeaders();
HttpStatus responseStatus;
UserCredentials userCredentials = userService.findUserCredentialsByActivateToken(TenantId.SYS_TENANT_ID, activateToken);
if (userCredentials != null) {
String createURI = "/login/createPassword";
try {
URI location = new URI(createURI + "?activateToken=" + activateToken);
headers.setLocation(location);
responseStatus = HttpStatus.SEE_OTHER;
} catch (URISyntaxException e) {
log.error("Unable to create URI with address [{}]", createURI);
responseStatus = HttpStatus.BAD_REQUEST;
}
} else {
responseStatus = HttpStatus.CONFLICT;
if (userCredentials == null) {
return response(HttpStatus.CONFLICT);
} else if (userCredentials.isActivationTokenExpired()) {
return redirectTo("/activationLinkExpired");
}
return new ResponseEntity<>(headers, responseStatus);
return redirectTo("/login/createPassword?activateToken=" + activateToken);
}
@ApiOperation(value = "Request reset password email (requestResetPasswordByEmail)",
notes = "Request to send the reset password email if the user with specified email address is present in the database. " +
"Always return '200 OK' status for security purposes.")
@RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@PostMapping(value = "/noauth/resetPasswordByEmail")
public void requestResetPasswordByEmail(
@Parameter(description = "The JSON object representing the reset password email request.")
@RequestBody ResetPasswordEmailRequest resetPasswordByEmailRequest,
@ -177,7 +159,7 @@ public class AuthController extends BaseController {
String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
userCredentials.getResetToken());
mailService.sendResetPasswordEmailAsync(resetUrl, email);
mailService.sendResetPasswordEmailAsync(resetUrl, userCredentials.getResetTokenTtl(), email);
} catch (Exception e) {
log.warn("Error occurred: {}", e.getMessage());
}
@ -186,32 +168,22 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Check password reset token (checkResetToken)",
notes = "Checks the password reset token and forwards user to 'Reset Password' page. " +
"If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Reset Password' page and same 'resetToken' specified in the URL parameters. " +
"If token is not valid, returns '409 Conflict'.")
@RequestMapping(value = "/noauth/resetPassword", params = {"resetToken"}, method = RequestMethod.GET)
public ResponseEntity<String> checkResetToken(
"If token is not valid, returns '409 Conflict'. " +
"If token is expired, redirects to error page.")
@GetMapping(value = "/noauth/resetPassword", params = {"resetToken"})
public ResponseEntity<?> checkResetToken(
@Parameter(description = "The reset token string.")
@RequestParam(value = "resetToken") String resetToken) {
HttpHeaders headers = new HttpHeaders();
HttpStatus responseStatus;
String resetURI = "/login/resetPassword";
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
if (userCredentials != null) {
if (!rateLimitService.checkRateLimit(LimitedApi.PASSWORD_RESET, userCredentials.getUserId(), defaultLimitsConfiguration)) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
}
try {
URI location = new URI(resetURI + "?resetToken=" + resetToken);
headers.setLocation(location);
responseStatus = HttpStatus.SEE_OTHER;
} catch (URISyntaxException e) {
log.error("Unable to create URI with address [{}]", resetURI);
responseStatus = HttpStatus.BAD_REQUEST;
}
} else {
responseStatus = HttpStatus.CONFLICT;
if (userCredentials == null) {
return response(HttpStatus.CONFLICT);
} else if (userCredentials.isResetTokenExpired()) {
return redirectTo("/passwordResetLinkExpired");
}
if (!rateLimitService.checkRateLimit(LimitedApi.PASSWORD_RESET, userCredentials.getUserId(), defaultLimitsConfiguration)) {
return response(HttpStatus.TOO_MANY_REQUESTS);
}
return new ResponseEntity<>(headers, responseStatus);
return redirectTo("/login/resetPassword?resetToken=" + resetToken);
}
@ApiOperation(value = "Activate User",
@ -220,15 +192,12 @@ public class AuthController extends BaseController {
"The response already contains the [JWT](https://jwt.io) activation and refresh tokens, " +
"to simplify the user activation flow and avoid asking user to input password again after activation. " +
"If token is valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " +
"If token is not valid, returns '404 Bad Request'.")
@RequestMapping(value = "/noauth/activate", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody
public JwtPair activateUser(
@Parameter(description = "Activate user request.")
@RequestBody ActivateUserRequest activateRequest,
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
HttpServletRequest request) throws ThingsboardException {
"If token is not valid, returns '400 Bad Request'.")
@PostMapping(value = "/noauth/activate")
public JwtPair activateUser(@Parameter(description = "Activate user request.")
@RequestBody ActivateUserRequest activateRequest,
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
HttpServletRequest request) {
String activateToken = activateRequest.getActivateToken();
String password = activateRequest.getPassword();
systemSecurityService.validatePassword(password, null);
@ -258,18 +227,18 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Reset password (resetPassword)",
notes = "Checks the password reset token and updates the password. " +
"If token is valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " +
"If token is not valid, returns '404 Bad Request'.")
@RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody
public JwtPair resetPassword(
@Parameter(description = "Reset password request.")
@RequestBody ResetPasswordRequest resetPasswordRequest,
HttpServletRequest request) throws ThingsboardException {
"If token is not valid, returns '400 Bad Request'.")
@PostMapping(value = "/noauth/resetPassword")
public JwtPair resetPassword(@Parameter(description = "Reset password request.")
@RequestBody ResetPasswordRequest resetPasswordRequest,
HttpServletRequest request) throws ThingsboardException {
String resetToken = resetPasswordRequest.getResetToken();
String password = resetPasswordRequest.getPassword();
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
if (userCredentials != null) {
if (userCredentials.isResetTokenExpired()) {
throw new ThingsboardException("Password reset token expired", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
systemSecurityService.validatePassword(password, userCredentials);
if (passwordEncoder.matches(password, userCredentials.getPassword())) {
throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
@ -277,6 +246,7 @@ public class AuthController extends BaseController {
String encodedPassword = passwordEncoder.encode(password);
userCredentials.setPassword(encodedPassword);
userCredentials.setResetToken(null);
userCredentials.setResetTokenExpTime(null);
userCredentials = userService.replaceUserCredentials(TenantId.SYS_TENANT_ID, userCredentials);
User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId());
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());

25
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -27,7 +27,9 @@ import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.MethodArgumentNotValidException;
@ -132,8 +134,8 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
import org.thingsboard.server.dao.oauth2.OAuth2ClientService;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
@ -170,8 +172,8 @@ import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@ -191,7 +193,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@TbCoreComponent
public abstract class BaseController {
private final Logger log = org.slf4j.LoggerFactory.getLogger(getClass());
protected final Logger log = org.slf4j.LoggerFactory.getLogger(getClass());
/*Swagger UI description*/
@ -912,6 +914,23 @@ public abstract class BaseController {
}
}
protected <T> ResponseEntity<T> response(HttpStatus status) {
return ResponseEntity.status(status).build();
}
protected <T> ResponseEntity<T> redirectTo(String location) {
URI uri;
try {
uri = URI.create(location);
} catch (IllegalArgumentException e) {
log.error("Failed to create URI from '{}'", location, e);
throw e;
}
return ResponseEntity.status(HttpStatus.SEE_OTHER)
.location(uri)
.build();
}
protected List<OAuth2ClientId> getOAuth2ClientIds(UUID[] ids) throws ThingsboardException {
if (ids == null) {
return Collections.emptyList();

2
application/src/main/java/org/thingsboard/server/controller/ImageController.java

@ -312,7 +312,7 @@ public class ImageController extends BaseController {
if (StringUtils.isNotEmpty(etag)) {
etag = StringUtils.remove(etag, '\"'); // etag is wrapped in double quotes due to HTTP specification
if (etag.equals(tbImageService.getETag(cacheKey))) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
return response(HttpStatus.NOT_MODIFIED);
}
}

3
application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java

@ -183,8 +183,7 @@ public class MobileApplicationController extends BaseController {
.header("Location", appStoreLink)
.build();
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.build();
return response(HttpStatus.NOT_FOUND);
}
}

1
application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java

@ -164,6 +164,7 @@ public class SystemInfoController extends BaseController {
} else {
infoObject.put("version", "unknown");
}
infoObject.put("type", "CE");
return infoObject;
}
}

69
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -21,7 +21,6 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus;
@ -44,6 +43,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserActivationLink;
import org.thingsboard.server.common.data.UserEmailInfo;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@ -119,7 +119,6 @@ public class UserController extends BaseController {
public static final String USER_ID = "userId";
public static final String PATHS = "paths";
public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!";
public static final String ACTIVATE_URL_PATTERN = "%s/api/noauth/activate?activateToken=%s";
public static final String MOBILE_TOKEN_HEADER = "X-Mobile-Token";
@Value("${security.user_token_access_enabled}")
@ -130,12 +129,8 @@ public class UserController extends BaseController {
private final SystemSecurityService systemSecurityService;
private final ApplicationEventPublisher eventPublisher;
private final TbUserService tbUserService;
@Autowired
private EntityQueryService entityQueryService;
@Autowired
private EntityService entityService;
private final EntityQueryService entityQueryService;
private final EntityService entityService;
@ApiOperation(value = "Get User (getUserById)",
notes = "Fetch the User object based on the provided User Id. " +
@ -212,7 +207,7 @@ public class UserController extends BaseController {
public User saveUser(
@Parameter(description = "A JSON value representing the User.", required = true)
@RequestBody User user,
@Parameter(description = "Send activation email (or use activation link)" , schema = @Schema(defaultValue = "true"))
@Parameter(description = "Send activation email (or use activation link)", schema = @Schema(defaultValue = "true"))
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, HttpServletRequest request) throws ThingsboardException {
if (!Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
user.setTenantId(getCurrentUser().getTenantId());
@ -230,45 +225,39 @@ public class UserController extends BaseController {
@Parameter(description = "Email of the user", required = true)
@RequestParam(value = "email") String email,
HttpServletRequest request) throws ThingsboardException {
User user = checkNotNull(userService.findUserByEmail(getCurrentUser().getTenantId(), email));
accessControlService.checkPermission(getCurrentUser(), Resource.USER, Operation.READ,
user.getId(), user);
SecurityUser securityUser = getCurrentUser();
User user = checkNotNull(userService.findUserByEmail(securityUser.getTenantId(), email));
accessControlService.checkPermission(securityUser, Resource.USER, Operation.READ, user.getId(), user);
UserCredentials userCredentials = userService.findUserCredentialsByUserId(getCurrentUser().getTenantId(), user.getId());
if (!userCredentials.isEnabled() && userCredentials.getActivateToken() != null) {
String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
userCredentials.getActivateToken());
mailService.sendActivationEmail(activateUrl, email);
} else {
throw new ThingsboardException("User is already activated!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
UserActivationLink activationLink = tbUserService.getActivationLink(securityUser.getTenantId(), securityUser.getCustomerId(), user.getId(), request);
mailService.sendActivationEmail(activationLink.value(), activationLink.ttlMs(), email);
}
@ApiOperation(value = "Get the activation link (getActivationLink)",
@ApiOperation(value = "Get activation link (getActivationLink)",
notes = "Get the activation link for the user. " +
"The base url for activation link is configurable in the general settings of system administrator. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/{userId}/activationLink", method = RequestMethod.GET, produces = "text/plain")
@GetMapping(value = "/user/{userId}/activationLink", produces = "text/plain")
@ResponseBody
public String getActivationLink(
@Parameter(description = USER_ID_PARAM_DESCRIPTION)
@PathVariable(USER_ID) String strUserId,
HttpServletRequest request) throws ThingsboardException {
public String getActivationLink(@Parameter(description = USER_ID_PARAM_DESCRIPTION)
@PathVariable(USER_ID) String strUserId,
HttpServletRequest request) throws ThingsboardException {
return getActivationLinkInfo(strUserId, request).value();
}
@ApiOperation(value = "Get activation link info (getActivationLinkInfo)",
notes = "Get the activation link info for the user. " +
"The base url for activation link is configurable in the general settings of system administrator. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/user/{userId}/activationLinkInfo")
public UserActivationLink getActivationLinkInfo(@Parameter(description = USER_ID_PARAM_DESCRIPTION)
@PathVariable(USER_ID) String strUserId,
HttpServletRequest request) throws ThingsboardException {
checkParameter(USER_ID, strUserId);
UserId userId = new UserId(toUUID(strUserId));
User user = checkUserId(userId, Operation.READ);
SecurityUser authUser = getCurrentUser();
UserCredentials userCredentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), user.getId());
if (!userCredentials.isEnabled() && userCredentials.getActivateToken() != null) {
String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
userCredentials.getActivateToken());
return activateUrl;
} else {
throw new ThingsboardException("User is already activated!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
checkUserId(userId, Operation.READ);
SecurityUser securityUser = getCurrentUser();
return tbUserService.getActivationLink(securityUser.getTenantId(), securityUser.getCustomerId(), userId, request);
}
@ApiOperation(value = "Delete User (deleteUser)",
@ -411,7 +400,7 @@ public class UserController extends BaseController {
public void setUserCredentialsEnabled(
@Parameter(description = USER_ID_PARAM_DESCRIPTION)
@PathVariable(USER_ID) String strUserId,
@Parameter(description = "Enable (\"true\") or disable (\"false\") the credentials." , schema = @Schema(defaultValue = "true"))
@Parameter(description = "Enable (\"true\") or disable (\"false\") the credentials.", schema = @Schema(defaultValue = "true"))
@RequestParam(required = false, defaultValue = "true") boolean userCredentialsEnabled) throws ThingsboardException {
checkParameter(USER_ID, strUserId);
UserId userId = new UserId(toUUID(strUserId));

13
application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java

@ -34,8 +34,10 @@ import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.EntityAlarm;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.domain.Domain;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
@ -134,6 +136,17 @@ public class EdgeEventSourcingListener {
return;
}
try {
if (event.getEntityId().getEntityType().equals(EntityType.RULE_CHAIN) && event.getEdgeId() != null && event.getActionType().equals(ActionType.ASSIGNED_TO_EDGE)) {
try {
Edge edge = JacksonUtil.fromString(event.getBody(), Edge.class);
if (edge != null && new RuleChainId(event.getEntityId().getId()).equals(edge.getRootRuleChainId())) {
log.trace("[{}] skipping ASSIGNED_TO_EDGE event of RULE_CHAIN entity in case Edge Root Rule Chain: {}", event.getTenantId(), event);
return;
}
} catch (Exception ignored) {
return;
}
}
log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event);
tbClusterService.sendNotificationMsgToEdge(event.getTenantId(), event.getEdgeId(), event.getEntityId(),
event.getBody(), null, EdgeUtils.getEdgeEventActionTypeByActionType(event.getActionType()),

2
application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java

@ -55,6 +55,7 @@ public class RelatedEdgesSourcingListener {
@TransactionalEventListener(fallbackExecution = true)
public void handleEvent(ActionEntityEvent<?> event) {
executorService.submit(() -> {
log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event);
try {
switch (event.getActionType()) {
case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE ->
@ -69,6 +70,7 @@ public class RelatedEdgesSourcingListener {
@TransactionalEventListener(fallbackExecution = true)
public void handleEvent(DeleteEntityEvent<?> event) {
executorService.submit(() -> {
log.trace("[{}] DeleteEntityEvent called: {}", event.getTenantId(), event);
try {
relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId());
} catch (Exception e) {

32
application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java

@ -22,7 +22,9 @@ import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserActivationLink;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@ -33,7 +35,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import static org.thingsboard.server.controller.UserController.ACTIVATE_URL_PATTERN;
import java.util.concurrent.TimeUnit;
@Service
@TbCoreComponent
@ -53,13 +55,9 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse
boolean sendEmail = tbUser.getId() == null && sendActivationMail;
User savedUser = checkNotNull(userService.saveUser(tenantId, tbUser));
if (sendEmail) {
UserCredentials userCredentials = userService.findUserCredentialsByUserId(tenantId, savedUser.getId());
String baseUrl = systemSecurityService.getBaseUrl(tenantId, customerId, request);
String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
userCredentials.getActivateToken());
String email = savedUser.getEmail();
UserActivationLink activationLink = getActivationLink(tenantId, customerId, savedUser.getId(), request);
try {
mailService.sendActivationEmail(activateUrl, email);
mailService.sendActivationEmail(activationLink.value(), activationLink.ttlMs(), savedUser.getEmail());
} catch (ThingsboardException e) {
userService.deleteUser(tenantId, savedUser);
throw e;
@ -87,4 +85,24 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse
throw e;
}
}
@Override
public UserActivationLink getActivationLink(TenantId tenantId, CustomerId customerId, UserId userId, HttpServletRequest request) throws ThingsboardException {
UserCredentials userCredentials = userService.findUserCredentialsByUserId(tenantId, userId);
if (!userCredentials.isEnabled() && userCredentials.getActivateToken() != null) {
long ttl = userCredentials.getActivationTokenTtl();
if (ttl < TimeUnit.MINUTES.toMillis(15)) { // renew link if less than 15 minutes before expiration
userCredentials = userService.generateUserActivationToken(userCredentials);
userCredentials = userService.saveUserCredentials(tenantId, userCredentials);
ttl = userCredentials.getActivationTokenTtl();
log.debug("[{}][{}] Regenerated expired user activation token", tenantId, userId);
}
String baseUrl = systemSecurityService.getBaseUrl(tenantId, customerId, request);
String link = baseUrl + "/api/noauth/activate?activateToken=" + userCredentials.getActivateToken();
return new UserActivationLink(link, ttl);
} else {
throw new ThingsboardException("User is already activated!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
}
}

5
application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java

@ -16,14 +16,19 @@
package org.thingsboard.server.service.entitiy.user;
import jakarta.servlet.http.HttpServletRequest;
import org.thingsboard.server.common.data.UserActivationLink;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
public interface TbUserService {
User save(TenantId tenantId, CustomerId customerId, User tbUser, boolean sendActivationMail, HttpServletRequest request, User user) throws ThingsboardException;
void delete(TenantId tenantId, CustomerId customerId, User user, User responsibleUser) throws ThingsboardException;
UserActivationLink getActivationLink(TenantId tenantId, CustomerId customerId, UserId userId, HttpServletRequest request) throws ThingsboardException;
}

1
application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java

@ -92,4 +92,5 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
}
cacheManager.getCacheNames().forEach(this::clearCacheByName);
}
}

11
application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java

@ -160,12 +160,12 @@ public class DefaultMailService implements MailService {
}
@Override
public void sendActivationEmail(String activationLink, String email) throws ThingsboardException {
public void sendActivationEmail(String activationLink, long ttlMs, String email) throws ThingsboardException {
String subject = messages.getMessage("activation.subject", null, Locale.US);
Map<String, Object> model = new HashMap<>();
model.put("activationLink", activationLink);
model.put("activationLinkTtlInHours", (int) Math.ceil(ttlMs / 3600000.0));
model.put(TARGET_EMAIL, email);
String message = mergeTemplateIntoString("activation.ftl", model);
@ -188,12 +188,13 @@ public class DefaultMailService implements MailService {
}
@Override
public void sendResetPasswordEmail(String passwordResetLink, String email) throws ThingsboardException {
public void sendResetPasswordEmail(String passwordResetLink, long ttlMs, String email) throws ThingsboardException {
String subject = messages.getMessage("reset.password.subject", null, Locale.US);
Map<String, Object> model = new HashMap<>();
model.put("passwordResetLink", passwordResetLink);
model.put("passwordResetLinkTtlInHours", (int) Math.ceil(ttlMs / 3600000.0));
model.put(TARGET_EMAIL, email);
String message = mergeTemplateIntoString("reset.password.ftl", model);
@ -202,10 +203,10 @@ public class DefaultMailService implements MailService {
}
@Override
public void sendResetPasswordEmailAsync(String passwordResetLink, String email) {
public void sendResetPasswordEmailAsync(String passwordResetLink, long ttlMs, String email) {
passwordResetExecutorService.execute(() -> {
try {
this.sendResetPasswordEmail(passwordResetLink, email);
this.sendResetPasswordEmail(passwordResetLink, ttlMs, email);
} catch (Exception e) {
log.error("Error occurred: {} ", e.getMessage());
}

2
application/src/main/java/org/thingsboard/server/service/mail/DefaultTbMailConfigTemplateService.java

@ -32,7 +32,7 @@ public class DefaultTbMailConfigTemplateService implements TbMailConfigTemplateS
@PostConstruct
private void postConstruct() throws IOException {
mailConfigTemplates = JacksonUtil.toJsonNode(new ClassPathResource("/templates/mail_config_templates.json").getFile());
mailConfigTemplates = JacksonUtil.toJsonNode(new ClassPathResource("/templates/mail_config_templates.json").getInputStream());
}
@Override

10
application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java

@ -25,11 +25,12 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.dao.entity.AbstractCachedService;
import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import static org.thingsboard.server.service.security.system.DefaultSystemSecurityService.DEFAULT_MOBILE_SECRET_KEY_LENGTH;
import static org.thingsboard.server.dao.settings.DefaultSecuritySettingsService.DEFAULT_MOBILE_SECRET_KEY_LENGTH;
@Service
@Slf4j
@ -37,12 +38,12 @@ import static org.thingsboard.server.service.security.system.DefaultSystemSecuri
public class MobileAppSecretServiceImpl extends AbstractCachedService<String, JwtPair, MobileSecretEvictEvent> implements MobileAppSecretService {
private final JwtTokenFactory tokenFactory;
private final SystemSecurityService systemSecurityService;
private final SecuritySettingsService securitySettingsService;
@Override
public String generateMobileAppSecret(SecurityUser securityUser) {
log.trace("Executing generateSecret for user [{}]", securityUser.getId());
Integer mobileSecretKeyLength = systemSecurityService.getSecuritySettings().getMobileSecretKeyLength();
Integer mobileSecretKeyLength = securitySettingsService.getSecuritySettings().getMobileSecretKeyLength();
String secret = StringUtils.generateSafeToken(mobileSecretKeyLength == null ? DEFAULT_MOBILE_SECRET_KEY_LENGTH : mobileSecretKeyLength);
cache.put(secret, tokenFactory.createTokenPair(securityUser));
return secret;
@ -63,4 +64,5 @@ public class MobileAppSecretServiceImpl extends AbstractCachedService<String, Jw
public void handleEvictEvent(MobileSecretEvictEvent event) {
cache.evict(event.getSecret());
}
}

8
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java

@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
@ -58,6 +59,7 @@ import java.util.UUID;
public class RestAuthenticationProvider implements AuthenticationProvider {
private final SystemSecurityService systemSecurityService;
private final SecuritySettingsService securitySettingsService;
private final UserService userService;
private final CustomerService customerService;
private final TwoFactorAuthService twoFactorAuthService;
@ -66,10 +68,12 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
public RestAuthenticationProvider(final UserService userService,
final CustomerService customerService,
final SystemSecurityService systemSecurityService,
SecuritySettingsService securitySettingsService,
TwoFactorAuthService twoFactorAuthService) {
this.userService = userService;
this.customerService = customerService;
this.systemSecurityService = systemSecurityService;
this.securitySettingsService = securitySettingsService;
this.twoFactorAuthService = twoFactorAuthService;
}
@ -82,13 +86,13 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
throw new BadCredentialsException("Authentication Failed. Bad user principal.");
}
UserPrincipal userPrincipal = (UserPrincipal) principal;
UserPrincipal userPrincipal = (UserPrincipal) principal;
SecurityUser securityUser;
if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) {
String username = userPrincipal.getValue();
String password = (String) authentication.getCredentials();
SecuritySettings securitySettings = systemSecurityService.getSecuritySettings();
SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy();
if (Boolean.TRUE.equals(passwordPolicy.getForceUserToResetPasswordIfNotValid())) {
try {

82
application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java

@ -18,8 +18,8 @@ package org.thingsboard.server.service.security.system;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.passay.CharacterRule;
import org.passay.EnglishCharacterData;
@ -29,9 +29,6 @@ import org.passay.PasswordValidator;
import org.passay.Rule;
import org.passay.RuleResult;
import org.passay.WhitespaceRule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
@ -55,6 +52,7 @@ import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettin
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.user.UserServiceImpl;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
@ -68,76 +66,23 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE;
@Service
@Slf4j
@RequiredArgsConstructor
public class DefaultSystemSecurityService implements SystemSecurityService {
public static final int DEFAULT_MOBILE_SECRET_KEY_LENGTH = 64;
@Autowired
private AdminSettingsService adminSettingsService;
@Autowired
private BCryptPasswordEncoder encoder;
@Autowired
private UserService userService;
@Autowired
private MailService mailService;
@Autowired
private AuditLogService auditLogService;
@Resource
private SystemSecurityService self;
@Cacheable(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'")
@Override
public SecuritySettings getSecuritySettings() {
SecuritySettings securitySettings = null;
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "securitySettings");
if (adminSettings != null) {
try {
securitySettings = JacksonUtil.convertValue(adminSettings.getJsonValue(), SecuritySettings.class);
} catch (Exception e) {
throw new RuntimeException("Failed to load security settings!", e);
}
} else {
securitySettings = new SecuritySettings();
securitySettings.setPasswordPolicy(new UserPasswordPolicy());
securitySettings.getPasswordPolicy().setMinimumLength(6);
securitySettings.getPasswordPolicy().setMaximumLength(72);
securitySettings.setMobileSecretKeyLength(DEFAULT_MOBILE_SECRET_KEY_LENGTH);
}
return securitySettings;
}
@CacheEvict(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'")
@Override
public SecuritySettings saveSecuritySettings(SecuritySettings securitySettings) {
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "securitySettings");
if (adminSettings == null) {
adminSettings = new AdminSettings();
adminSettings.setTenantId(TenantId.SYS_TENANT_ID);
adminSettings.setKey("securitySettings");
}
adminSettings.setJsonValue(JacksonUtil.valueToTree(securitySettings));
AdminSettings savedAdminSettings = adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings);
try {
return JacksonUtil.convertValue(savedAdminSettings.getJsonValue(), SecuritySettings.class);
} catch (Exception e) {
throw new RuntimeException("Failed to load security settings!", e);
}
}
private final AdminSettingsService adminSettingsService;
private final BCryptPasswordEncoder encoder;
private final UserService userService;
private final MailService mailService;
private final AuditLogService auditLogService;
private final SecuritySettingsService securitySettingsService;
@Override
public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException {
if (!encoder.matches(password, userCredentials.getPassword())) {
int failedLoginAttempts = userService.increaseFailedLoginAttempts(tenantId, userCredentials.getUserId());
SecuritySettings securitySettings = self.getSecuritySettings();
SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) {
if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) {
lockAccount(userCredentials.getUserId(), username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts());
@ -153,7 +98,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
userService.resetFailedLoginAttempts(tenantId, userCredentials.getUserId());
SecuritySettings securitySettings = self.getSecuritySettings();
SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) {
if ((userCredentials.getCreatedTime()
+ TimeUnit.DAYS.toMillis(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays()))
@ -181,7 +126,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
if (maxVerificationFailures != null && maxVerificationFailures > 0
&& failedVerificationAttempts >= maxVerificationFailures) {
userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false);
SecuritySettings securitySettings = self.getSecuritySettings();
SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
lockAccount(userId, securityUser.getEmail(), securitySettings.getUserLockoutNotificationEmail(), maxVerificationFailures);
throw new LockedException("User account was locked due to exceeded 2FA verification attempts");
}
@ -200,7 +145,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
@Override
public void validatePassword(String password, UserCredentials userCredentials) throws DataValidationException {
SecuritySettings securitySettings = self.getSecuritySettings();
SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy();
validatePasswordByPolicy(password, passwordPolicy);
@ -330,4 +275,5 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
private static boolean isPositiveInteger(Integer val) {
return val != null && val.intValue() > 0;
}
}

6
application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java

@ -22,7 +22,6 @@ import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.dao.exception.DataValidationException;
@ -30,10 +29,6 @@ import org.thingsboard.server.service.security.model.SecurityUser;
public interface SystemSecurityService {
SecuritySettings getSecuritySettings();
SecuritySettings saveSecuritySettings(SecuritySettings securitySettings);
void validatePasswordByPolicy(String password, UserPasswordPolicy passwordPolicy);
void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException;
@ -47,4 +42,5 @@ public interface SystemSecurityService {
void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e);
void logLoginAction(User user, Object authenticationDetails, ActionType actionType, String provider, Exception e);
}

10
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java

@ -94,7 +94,7 @@ import java.util.stream.Collectors;
public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubscriptionService {
private static final int DEFAULT_LIMIT = 100;
private final Map<String, Map<Integer, TbAbstractSubCtx>> subscriptionsBySessionId = new ConcurrentHashMap<>();
private final ConcurrentMap<String, ConcurrentMap<Integer, TbAbstractSubCtx>> subscriptionsBySessionId = new ConcurrentHashMap<>();
@Autowired
@Lazy
@ -495,7 +495,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
}
private TbEntityDataSubCtx createSubCtx(WebSocketSessionRef sessionRef, EntityDataCmd cmd) {
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new ConcurrentHashMap<>());
TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription);
if (cmd.getQuery() != null) {
@ -506,7 +506,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
}
private TbEntityCountSubCtx createSubCtx(WebSocketSessionRef sessionRef, EntityCountCmd cmd) {
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new ConcurrentHashMap<>());
TbEntityCountSubCtx ctx = new TbEntityCountSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, sessionRef, cmd.getCmdId());
if (cmd.getQuery() != null) {
@ -518,7 +518,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
private TbAlarmDataSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new ConcurrentHashMap<>());
TbAlarmDataSubCtx ctx = new TbAlarmDataSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription,
maxAlarmQueriesPerRefreshInterval);
@ -528,7 +528,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
}
private TbAlarmCountSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) {
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new ConcurrentHashMap<>());
TbAlarmCountSubCtx ctx = new TbAlarmCountSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, alarmService, sessionRef, cmd.getCmdId());
if (cmd.getQuery() != null) {

2
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java

@ -85,7 +85,7 @@ import java.util.stream.Collectors;
@Service
public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionService {
private final ConcurrentMap<String, Map<Integer, TbSubscription<?>>> subscriptionsBySessionId = new ConcurrentHashMap<>();
private final ConcurrentMap<String, ConcurrentMap<Integer, TbSubscription<?>>> subscriptionsBySessionId = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, TbEntityLocalSubsInfo> subscriptionsByEntityId = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, TbEntityUpdatesInfo> entityUpdates = new ConcurrentHashMap<>();

2
application/src/main/resources/templates/activation.ftl

@ -88,7 +88,7 @@ background-color: #f6f6f6;
</tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
To confirm your email address and choose a password, just click the button below.
To confirm your email address and choose a password, just click the button below. The link will expire in ${activationLinkTtlInHours} hours.
</td>
</tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">

2
application/src/main/resources/templates/reset.password.ftl

@ -93,7 +93,7 @@ background-color: #f6f6f6;
</tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
Click below in order to proceed password reset procedure.
Click below in order to proceed password reset procedure. The link will expire in ${passwordResetLinkTtlInHours} hours.
</td>
</tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">

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

@ -1658,6 +1658,8 @@ queue:
connection_timeout: "${TB_QUEUE_RABBIT_MQ_CONNECTION_TIMEOUT:60000}"
# RabbitMQ has a timeout for connection handshake. When clients run in heavily constrained environments, it may be necessary to increase the timeout
handshake_timeout: "${TB_QUEUE_RABBIT_MQ_HANDSHAKE_TIMEOUT:10000}"
# The maximum number of messages returned in a single call of doPoll() method
max_poll_messages: "${TB_QUEUE_RABBIT_MQ_MAX_POLL_MESSAGES:1}"
queue-properties:
# RabbitMQ properties for Rule Engine queues
rule-engine: "${TB_QUEUE_RABBIT_MQ_RE_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}"

5
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -145,6 +145,7 @@ import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
@ -340,7 +341,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
currentActivateToken = activationLink.split("=")[1];
return null;
}
}).when(mailService).sendActivationEmail(anyString(), anyString());
}).when(mailService).sendActivationEmail(anyString(), anyLong(), anyString());
Mockito.doAnswer(new Answer<Void>() {
public Void answer(InvocationOnMock invocation) {
@ -349,7 +350,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
currentResetPasswordToken = passwordResetLink.split("=")[1];
return null;
}
}).when(mailService).sendResetPasswordEmailAsync(anyString(), anyString());
}).when(mailService).sendResetPasswordEmailAsync(anyString(), anyLong(), anyString());
}
@After

184
application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java

@ -16,20 +16,30 @@
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import org.assertj.core.data.Offset;
import org.junit.After;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.http.HttpHeaders;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserActivationLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.user.UserCredentialsDao;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.model.ChangePasswordRequest;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.anyString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
@ -39,55 +49,55 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@DaoSqlTest
public class AuthControllerTest extends AbstractControllerTest {
@SpyBean
private UserCredentialsDao userCredentialsDao;
@After
public void tearDown() throws Exception {
loginSysAdmin();
SecuritySettings securitySettings = doGet("/api/admin/securitySettings", SecuritySettings.class);
securitySettings.getPasswordPolicy().setMaximumLength(72);
securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(false);
doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk());
updateSecuritySettings(securitySettings -> {
securitySettings.getPasswordPolicy().setMaximumLength(72);
securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(false);
});
}
@Test
public void testGetUser() throws Exception {
doGet("/api/auth/user")
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized());
loginSysAdmin();
doGet("/api/auth/user")
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
loginTenantAdmin();
doGet("/api/auth/user")
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority",is(Authority.TENANT_ADMIN.name())))
.andExpect(jsonPath("$.email",is(TENANT_ADMIN_EMAIL)));
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name())))
.andExpect(jsonPath("$.email", is(TENANT_ADMIN_EMAIL)));
loginCustomerUser();
doGet("/api/auth/user")
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority",is(Authority.CUSTOMER_USER.name())))
.andExpect(jsonPath("$.email",is(CUSTOMER_USER_EMAIL)));
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority", is(Authority.CUSTOMER_USER.name())))
.andExpect(jsonPath("$.email", is(CUSTOMER_USER_EMAIL)));
}
@Test
public void testLoginLogout() throws Exception {
loginSysAdmin();
doGet("/api/auth/user")
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
TimeUnit.SECONDS.sleep(1); //We need to make sure that event for invalidating token was successfully processed
logout();
doGet("/api/auth/user")
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized());
resetTokens();
}
@ -97,14 +107,14 @@ public class AuthControllerTest extends AbstractControllerTest {
loginSysAdmin();
doGet("/api/auth/user")
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
refreshToken();
doGet("/api/auth/user")
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
}
@Test
@ -131,10 +141,10 @@ public class AuthControllerTest extends AbstractControllerTest {
loginUser(TENANT_ADMIN_EMAIL, newPassword);
loginSysAdmin();
SecuritySettings securitySettings = doGet("/api/admin/securitySettings", SecuritySettings.class);
securitySettings.getPasswordPolicy().setMaximumLength(15);
securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(true);
doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk());
updateSecuritySettings(securitySettings -> {
securitySettings.getPasswordPolicy().setMaximumLength(15);
securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(true);
});
//try to login with user password that is not valid after security settings was updated
doPost("/api/auth/login", new LoginRequest(TENANT_ADMIN_EMAIL, newPassword))
@ -142,6 +152,7 @@ public class AuthControllerTest extends AbstractControllerTest {
.andExpect(jsonPath("$.message", is("The entered password violates our policies. If this is your real password, please reset it.")));
}
@Test
public void testShouldNotResetPasswordToTooLongValue() throws Exception {
loginTenantAdmin();
@ -163,9 +174,95 @@ public class AuthControllerTest extends AbstractControllerTest {
Mockito.doNothing().when(mailService).sendPasswordWasResetEmail(anyString(), anyString());
doPost("/api/noauth/resetPassword", resetPasswordRequest)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message",
is("Password must be no more than 72 characters in length.")));
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message",
is("Password must be no more than 72 characters in length.")));
}
@Test
public void testPasswordResetLinkTtl() throws Exception {
loginSysAdmin();
int ttl = 24;
updateSecuritySettings(securitySettings -> {
securitySettings.setPasswordResetTokenTtl(ttl);
});
doPost("/api/noauth/resetPasswordByEmail", JacksonUtil.newObjectNode()
.put("email", TENANT_ADMIN_EMAIL)).andExpect(status().isOk());
UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, tenantAdminUserId.getId());
assertThat(userCredentials.getResetTokenExpTime()).isCloseTo(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(ttl), Offset.offset(120000L));
userCredentials.setResetTokenExpTime(System.currentTimeMillis() - 1);
userCredentialsDao.save(tenantId, userCredentials);
doGet("/api/noauth/resetPassword?resetToken={resetToken}", this.currentResetPasswordToken)
.andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/passwordResetLinkExpired"));
JsonNode resetPasswordRequest = JacksonUtil.newObjectNode()
.put("resetToken", this.currentResetPasswordToken)
.put("password", "wefwefe");
doPost("/api/noauth/resetPassword", resetPasswordRequest).andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message", is("Password reset token expired")));
}
@Test
public void testActivationLinkTtl() throws Exception {
loginSysAdmin();
int ttl = 24;
updateSecuritySettings(securitySettings -> {
securitySettings.setUserActivationTokenTtl(ttl);
});
loginTenantAdmin();
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setEmail("tenant-admin-2@thingsboard.org");
user = doPost("/api/user", user, User.class);
UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, user.getUuidId());
assertThat(userCredentials.getActivateTokenExpTime()).isCloseTo(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(ttl), Offset.offset(120000L));
String initialActivationLink = getActivationLink(user);
String initialActivationToken = StringUtils.substringAfterLast(initialActivationLink, "activateToken=");
UserActivationLink activationLinkInfo = getActivationLinkInfo(user);
assertThat(TimeUnit.MILLISECONDS.toHours(activationLinkInfo.ttlMs())).isCloseTo(ttl, within(1L));
assertThat(activationLinkInfo.value()).isEqualTo(initialActivationLink);
// expiring activation token
userCredentials.setActivateTokenExpTime(System.currentTimeMillis() - 1);
userCredentialsDao.save(tenantId, userCredentials);
doGet("/api/noauth/activate?activateToken={activateToken}", initialActivationToken)
.andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/activationLinkExpired"));
doPost("/api/noauth/activate", JacksonUtil.newObjectNode()
.put("activateToken", initialActivationToken)
.put("password", "wefewe")).andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message", is("Activation token expired")));
// checking that activation link is regenerated when requested
UserActivationLink regeneratedActivationLink = getActivationLinkInfo(user);
assertThat(regeneratedActivationLink.value()).isNotEqualTo(initialActivationLink);
assertThat(TimeUnit.MILLISECONDS.toHours(regeneratedActivationLink.ttlMs())).isCloseTo(ttl, within(1L));
// checking link renewal if less than 15 minutes before expiration
userCredentials = userCredentialsDao.findByUserId(tenantId, user.getUuidId());
userCredentials.setActivateTokenExpTime(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(30));
userCredentialsDao.save(tenantId, userCredentials);
activationLinkInfo = getActivationLinkInfo(user);
assertThat(activationLinkInfo.value()).isEqualTo(regeneratedActivationLink.value());
assertThat(TimeUnit.MILLISECONDS.toMinutes(activationLinkInfo.ttlMs())).isCloseTo(30, within(1L));
userCredentials.setActivateTokenExpTime(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(10));
userCredentialsDao.save(tenantId, userCredentials);
UserActivationLink newActivationLink = getActivationLinkInfo(user);
assertThat(newActivationLink.value()).isNotEqualTo(regeneratedActivationLink.value());
assertThat(TimeUnit.MILLISECONDS.toHours(newActivationLink.ttlMs())).isCloseTo(ttl, within(1L));
String newActivationToken = StringUtils.substringAfterLast(newActivationLink.value(), "activateToken=");
userCredentials = userCredentialsDao.findByUserId(tenantId, user.getUuidId());
assertThat(userCredentials.getActivateTokenExpTime()).isCloseTo(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(ttl), Offset.offset(120000L));
doPost("/api/noauth/activate", JacksonUtil.newObjectNode()
.put("activateToken", newActivationToken)
.put("password", "wefewe")).andExpect(status().isOk());
}
@Test
@ -173,4 +270,19 @@ public class AuthControllerTest extends AbstractControllerTest {
doGet("/login").andExpect(status().isOk());
doGet("/home").andExpect(status().isOk());
}
private void updateSecuritySettings(Consumer<SecuritySettings> updater) throws Exception {
SecuritySettings securitySettings = doGet("/api/admin/securitySettings", SecuritySettings.class);
updater.accept(securitySettings);
doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk());
}
private String getActivationLink(User user) throws Exception {
return doGet("/api/user/" + user.getId() + "/activationLink", String.class);
}
private UserActivationLink getActivationLinkInfo(User user) throws Exception {
return doGet("/api/user/" + user.getId() + "/activationLinkInfo", UserActivationLink.class);
}
}

25
application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java

@ -103,6 +103,7 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -124,6 +125,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
protected EdgeImitator edgeImitator;
protected Edge edge;
private Random random = new Random();
@Autowired
protected EdgeEventService edgeEventService;
@ -163,15 +166,10 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
}
private RuleChainId getEdgeRootRuleChainId() throws Exception {
List<RuleChain> edgeRuleChains = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/ruleChains?",
new TypeReference<PageData<RuleChain>>() {
}, new PageLink(100)).getData();
for (RuleChain edgeRuleChain : edgeRuleChains) {
if (edgeRuleChain.isRoot()) {
return edgeRuleChain.getId();
}
}
throw new RuntimeException("Root rule chain not found");
return doGetTypedWithPageLink("/api/ruleChains?type={type}&", new TypeReference<PageData<RuleChain>>() {},
new PageLink(100, 0, "Edge Root Rule Chain"),
"EDGE")
.getData().get(0).getId();
}
@After
@ -196,6 +194,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
Asset savedAsset = saveAsset("Edge Asset 1");
updateRootRuleChainMetadata();
edge = doPost("/api/edge", constructEdge("Test Edge", "test"), Edge.class);
doPost("/api/edge/" + edge.getUuidId()
@ -207,6 +207,13 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
TimeUnit.MILLISECONDS.sleep(1000);
}
protected void updateRootRuleChainMetadata() throws Exception {
RuleChainId rootRuleChainId = getEdgeRootRuleChainId();
RuleChainMetaData rootRuleChainMetadata = doGet("/api/ruleChain/" + rootRuleChainId.getId().toString() + "/metadata", RuleChainMetaData.class);
rootRuleChainMetadata.getNodes().forEach(n -> n.setDebugMode(random.nextBoolean()));
doPost("/api/ruleChain/metadata", rootRuleChainMetadata, RuleChainMetaData.class);
}
protected void extendDeviceProfileData(DeviceProfile deviceProfile) {
DeviceProfileData profileData = deviceProfile.getProfileData();
List<DeviceProfileAlarm> alarms = new ArrayList<>();

18
application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
@ -44,12 +45,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
public class DashboardEdgeTest extends AbstractEdgeTest {
private static final int MOBILE_ORDER = 5;
private static final String IMAGE = "data:image/png;base64,iVBORw0KGgoA";
private static final String IMAGE = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+";
@Test
public void testDashboards() throws Exception {
// create dashboard and assign to edge
edgeImitator.expectMessageAmount(1);
edgeImitator.expectMessageAmount(2);
Dashboard dashboard = new Dashboard();
dashboard.setTitle("Edge Test Dashboard");
dashboard.setMobileHide(true);
@ -59,23 +60,26 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
doPost("/api/edge/" + edge.getUuidId()
+ "/dashboard/" + savedDashboard.getUuidId(), Dashboard.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg);
DashboardUpdateMsg dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage;
Optional<DashboardUpdateMsg> dashboardUpdateMsgOpt = edgeImitator.findMessageByType(DashboardUpdateMsg.class);
Assert.assertTrue(dashboardUpdateMsgOpt.isPresent());
DashboardUpdateMsg dashboardUpdateMsg = dashboardUpdateMsgOpt.get();
Dashboard dashboardMsg = JacksonUtil.fromString(dashboardUpdateMsg.getEntity(), Dashboard.class, true);
Assert.assertNotNull(dashboardMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType());
Assert.assertEquals(savedDashboard, dashboardMsg);
Assert.assertEquals(IMAGE, dashboardMsg.getImage());
Assert.assertEquals("tb-image;/api/images/tenant/edge_test_dashboard_dashboard_image.svg", dashboardMsg.getImage());
Assert.assertEquals(MOBILE_ORDER, dashboardMsg.getMobileOrder().intValue());
testAutoGeneratedCodeByProtobuf(dashboardUpdateMsg);
Optional<ResourceUpdateMsg> resourceUpdateMsg = edgeImitator.findMessageByType(ResourceUpdateMsg.class);
Assert.assertTrue(resourceUpdateMsg.isPresent());
// update dashboard
edgeImitator.expectMessageAmount(1);
savedDashboard.setTitle("Updated Edge Test Dashboard");
savedDashboard = doPost("/api/dashboard", savedDashboard, Dashboard.class);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg);
dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage;
dashboardMsg = JacksonUtil.fromString(dashboardUpdateMsg.getEntity(), Dashboard.class, true);

12
application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java

@ -185,6 +185,18 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
return doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class);
}
@Test
public void testUpdateRootRuleChain() throws Exception {
edgeImitator.expectMessageAmount(2);
updateRootRuleChainMetadata();
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<RuleChainUpdateMsg> ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class);
Assert.assertTrue(ruleChainUpdateMsgOpt.isPresent());
Optional<RuleChainMetadataUpdateMsg> ruleChainMetadataUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainMetadataUpdateMsg.class);
Assert.assertTrue(ruleChainMetadataUpdateMsgOpt.isPresent());
}
@Test
public void testSetRootRuleChain() throws Exception {
// create rule chain

5
common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java

@ -54,11 +54,6 @@ public abstract class CaffeineTbTransactionalCache<K extends Serializable, V ext
return SimpleTbCacheValueWrapper.wrap(cache.get(key));
}
@Override
public TbCacheValueWrapper<V> get(K key, boolean transactionMode) {
return get(key);
}
@Override
public void put(K key, V value) {
lock.lock();

2
common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java

@ -31,7 +31,7 @@ public class RedisTbCacheTransaction<K extends Serializable, V extends Serializa
@Override
public void put(K key, V value) {
cache.put(key, value, connection, true);
cache.put(key, value, connection);
}
@Override

16
common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java

@ -87,17 +87,11 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
@Override
public TbCacheValueWrapper<V> get(K key) {
return get(key, false);
}
@Override
public TbCacheValueWrapper<V> get(K key, boolean transactionMode) {
if (!cacheEnabled) {
return null;
}
try (var connection = connectionFactory.getConnection()) {
byte[] rawKey = getRawKey(key);
byte[] rawValue = doGet(connection, rawKey, transactionMode);
byte[] rawValue = doGet(key, connection);
if (rawValue == null || rawValue.length == 0) {
return null;
} else if (Arrays.equals(rawValue, BINARY_NULL_VALUE)) {
@ -114,8 +108,8 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
}
}
protected byte[] doGet(RedisConnection connection, byte[] rawKey, boolean transactionMode) {
return connection.stringCommands().get(rawKey);
protected byte[] doGet(K key, RedisConnection connection) {
return connection.stringCommands().get(getRawKey(key));
}
@Override
@ -124,11 +118,11 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
return;
}
try (var connection = connectionFactory.getConnection()) {
put(key, value, connection, false);
put(key, value, connection);
}
}
public void put(K key, V value, RedisConnection connection, boolean transactionMode) {
public void put(K key, V value, RedisConnection connection) {
put(connection, key, value, RedisStringCommands.SetOption.UPSERT);
}

8
common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java

@ -27,8 +27,6 @@ public interface TbTransactionalCache<K extends Serializable, V extends Serializ
TbCacheValueWrapper<V> get(K key);
TbCacheValueWrapper<V> get(K key, boolean transactionMode);
void put(K key, V value);
void putIfAbsent(K key, V value);
@ -53,7 +51,7 @@ public interface TbTransactionalCache<K extends Serializable, V extends Serializ
if (putToCache) {
return getAndPutInTransaction(key, dbCall, cacheNullValue);
} else {
TbCacheValueWrapper<V> cacheValueWrapper = get(key, true);
TbCacheValueWrapper<V> cacheValueWrapper = get(key);
if (cacheValueWrapper != null) {
return cacheValueWrapper.get();
}
@ -66,7 +64,7 @@ public interface TbTransactionalCache<K extends Serializable, V extends Serializ
}
default <R> R getAndPutInTransaction(K key, Supplier<R> dbCall, Function<V, R> cacheValueToResult, Function<R, V> dbValueToCacheValue, boolean cacheNullValue) {
TbCacheValueWrapper<V> cacheValueWrapper = get(key, true);
TbCacheValueWrapper<V> cacheValueWrapper = get(key);
if (cacheValueWrapper != null) {
V cacheValue = cacheValueWrapper.get();
return cacheValue != null ? cacheValueToResult.apply(cacheValue) : null;
@ -92,7 +90,7 @@ public interface TbTransactionalCache<K extends Serializable, V extends Serializ
if (putToCache) {
return getAndPutInTransaction(key, dbCall, cacheValueToResult, dbValueToCacheValue, cacheNullValue);
} else {
TbCacheValueWrapper<V> cacheValueWrapper = get(key, true);
TbCacheValueWrapper<V> cacheValueWrapper = get(key);
if (cacheValueWrapper != null) {
var cacheValue = cacheValueWrapper.get();
return cacheValue == null ? null : cacheValueToResult.apply(cacheValue);

26
common/cache/src/main/java/org/thingsboard/server/cache/VersionedCacheKey.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2024 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.cache;
import java.io.Serializable;
public interface VersionedCacheKey extends Serializable {
default boolean isVersioned() {
return false;
}
}

2
common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java

@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.util.TbPair;
import java.io.Serializable;
public abstract class VersionedCaffeineTbCache<K extends Serializable, V extends Serializable & HasVersion> extends CaffeineTbTransactionalCache<K, V> implements VersionedTbCache<K, V> {
public abstract class VersionedCaffeineTbCache<K extends VersionedCacheKey, V extends Serializable & HasVersion> extends CaffeineTbTransactionalCache<K, V> implements VersionedTbCache<K, V> {
public VersionedCaffeineTbCache(CacheManager cacheManager, String cacheName) {
super(cacheManager, cacheName);

19
common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java

@ -30,7 +30,7 @@ import java.io.Serializable;
import java.util.Arrays;
@Slf4j
public abstract class VersionedRedisTbCache<K extends Serializable, V extends Serializable & HasVersion> extends RedisTbTransactionalCache<K, V> implements VersionedTbCache<K, V> {
public abstract class VersionedRedisTbCache<K extends VersionedCacheKey, V extends Serializable & HasVersion> extends RedisTbTransactionalCache<K, V> implements VersionedTbCache<K, V> {
private static final int VERSION_SIZE = 8;
private static final int VALUE_END_OFFSET = -1;
@ -79,15 +79,20 @@ public abstract class VersionedRedisTbCache<K extends Serializable, V extends Se
}
@Override
protected byte[] doGet(RedisConnection connection, byte[] rawKey, boolean transactionMode) {
if (transactionMode) {
return super.doGet(connection, rawKey, true);
protected byte[] doGet(K key, RedisConnection connection) {
if (!key.isVersioned()) {
return super.doGet(key, connection);
}
byte[] rawKey = getRawKey(key);
return connection.stringCommands().getRange(rawKey, VERSION_SIZE, VALUE_END_OFFSET);
}
@Override
public void put(K key, V value) {
if (!key.isVersioned()) {
super.put(key, value);
return;
}
Long version = getVersion(value);
if (version == null) {
return;
@ -96,9 +101,9 @@ public abstract class VersionedRedisTbCache<K extends Serializable, V extends Se
}
@Override
public void put(K key, V value, RedisConnection connection, boolean transactionMode) {
if (transactionMode) {
super.put(key, value, connection, true); // because scripting commands are not supported in transaction mode
public void put(K key, V value, RedisConnection connection) {
if (!key.isVersioned()) {
super.put(key, value, connection); // because scripting commands are not supported in transaction mode
return;
}
Long version = getVersion(value);

2
common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbCache.java

@ -22,7 +22,7 @@ import java.util.Collection;
import java.util.Optional;
import java.util.function.Supplier;
public interface VersionedTbCache<K extends Serializable, V extends Serializable & HasVersion> extends TbTransactionalCache<K, V> {
public interface VersionedTbCache<K extends VersionedCacheKey, V extends Serializable & HasVersion> extends TbTransactionalCache<K, V> {
TbCacheValueWrapper<V> get(K key);

9
common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java

@ -19,17 +19,17 @@ import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.cache.VersionedCacheKey;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.io.Serializable;
@Getter
@EqualsAndHashCode
@RequiredArgsConstructor
@Builder
public class DeviceCacheKey implements Serializable {
public class DeviceCacheKey implements VersionedCacheKey {
@Serial
private static final long serialVersionUID = 6366389552842340207L;
@ -61,4 +61,9 @@ public class DeviceCacheKey implements Serializable {
}
}
@Override
public boolean isVersioned() {
return deviceId != null;
}
}

4
common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java

@ -59,6 +59,10 @@ public interface UserService extends EntityDaoService {
UserCredentials requestExpiredPasswordReset(TenantId tenantId, UserCredentialsId userCredentialsId);
UserCredentials generatePasswordResetToken(UserCredentials userCredentials);
UserCredentials generateUserActivationToken(UserCredentials userCredentials);
UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials);
void deleteUser(TenantId tenantId, User user);

19
common/data/src/main/java/org/thingsboard/server/common/data/UserActivationLink.java

@ -0,0 +1,19 @@
/**
* Copyright © 2016-2024 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.common.data;
public record UserActivationLink(String value, long ttlMs) {
}

104
common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java

@ -16,41 +16,31 @@
package org.thingsboard.server.common.data.security;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.BaseData;
import lombok.ToString;
import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo;
import org.thingsboard.server.common.data.id.UserCredentialsId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.validation.NoXss;
import static org.thingsboard.server.common.data.BaseDataWithAdditionalInfo.getJson;
import static org.thingsboard.server.common.data.BaseDataWithAdditionalInfo.setJson;
import java.io.Serial;
@Data
@EqualsAndHashCode(callSuper = true)
public class UserCredentials extends BaseData<UserCredentialsId> {
@ToString(callSuper = true)
public class UserCredentials extends BaseDataWithAdditionalInfo<UserCredentialsId> {
@Serial
private static final long serialVersionUID = -2108436378880529163L;
private UserId userId;
private boolean enabled;
private String password;
private String activateToken;
private Long activateTokenExpTime;
private String resetToken;
private Long resetTokenExpTime;
@NoXss
private transient JsonNode additionalInfo;
@JsonIgnore
private byte[] additionalInfoBytes;
public JsonNode getAdditionalInfo() {
return getJson(() -> additionalInfo, () -> additionalInfoBytes);
}
public void setAdditionalInfo(JsonNode settings) {
setJson(settings, json -> this.additionalInfo = json, bytes -> this.additionalInfoBytes = bytes);
}
public UserCredentials() {
super();
}
@ -59,75 +49,25 @@ public class UserCredentials extends BaseData<UserCredentialsId> {
super(id);
}
public UserCredentials(UserCredentials userCredentials) {
super(userCredentials);
this.userId = userCredentials.getUserId();
this.password = userCredentials.getPassword();
this.enabled = userCredentials.isEnabled();
this.activateToken = userCredentials.getActivateToken();
this.resetToken = userCredentials.getResetToken();
setAdditionalInfo(userCredentials.getAdditionalInfo());
}
public UserId getUserId() {
return userId;
}
public void setUserId(UserId userId) {
this.userId = userId;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getActivateToken() {
return activateToken;
@JsonIgnore
public boolean isActivationTokenExpired() {
return getActivationTokenTtl() == 0;
}
public void setActivateToken(String activateToken) {
this.activateToken = activateToken;
}
public String getResetToken() {
return resetToken;
@JsonIgnore
public long getActivationTokenTtl() {
return activateTokenExpTime != null ? Math.max(activateTokenExpTime - System.currentTimeMillis(), 0) : 0;
}
public void setResetToken(String resetToken) {
this.resetToken = resetToken;
@JsonIgnore
public boolean isResetTokenExpired() {
return getResetTokenTtl() == 0;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("UserCredentials [userId=");
builder.append(userId);
builder.append(", enabled=");
builder.append(enabled);
builder.append(", password=");
builder.append(password);
builder.append(", activateToken=");
builder.append(activateToken);
builder.append(", resetToken=");
builder.append(resetToken);
builder.append(", createdTime=");
builder.append(createdTime);
builder.append(", id=");
builder.append(id);
builder.append("]");
return builder.toString();
@JsonIgnore
public long getResetTokenTtl() {
return resetTokenExpTime != null ? Math.max(resetTokenExpTime - System.currentTimeMillis(), 0) : 0;
}
}

25
common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java

@ -16,22 +16,39 @@
package org.thingsboard.server.common.data.security.model;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Schema
@Data
public class SecuritySettings implements Serializable {
@Serial
private static final long serialVersionUID = -1307613974597312465L;
@Schema(description = "The user password policy object." )
@Schema(description = "The user password policy object.")
private UserPasswordPolicy passwordPolicy;
@Schema(description = "Maximum number of failed login attempts allowed before user account is locked." )
@Schema(description = "Maximum number of failed login attempts allowed before user account is locked.")
private Integer maxFailedLoginAttempts;
@Schema(description = "Email to use for notifications about locked users." )
@Schema(description = "Email to use for notifications about locked users.")
private String userLockoutNotificationEmail;
@Schema(description = "Mobile secret key length" )
@Schema(description = "Mobile secret key length")
private Integer mobileSecretKeyLength;
@NotNull @Min(1) @Max(24)
@Schema(description = "TTL in hours for user activation link", minimum = "1", maximum = "24", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer userActivationTokenTtl;
@NotNull @Min(1) @Max(24)
@Schema(description = "TTL in hours for password reset link", minimum = "1", maximum = "24", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer passwordResetTokenTtl;
}

32
common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplate.java

@ -20,6 +20,8 @@ import com.google.protobuf.InvalidProtocolBufferException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.GetResponse;
import java.util.ArrayList;
import java.util.Collection;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin;
@ -31,7 +33,6 @@ import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
@ -44,6 +45,7 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTb
private final TbQueueMsgDecoder<T> decoder;
private final Channel channel;
private final Connection connection;
private final int maxPollMessages;
private volatile Set<String> queues;
@ -51,6 +53,7 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTb
super(topic);
this.admin = admin;
this.decoder = decoder;
this.maxPollMessages = rabbitMqSettings.getMaxPollMessages();
try {
connection = rabbitMqSettings.getConnectionFactory().newConnection();
} catch (IOException | TimeoutException e) {
@ -70,13 +73,19 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTb
protected List<GetResponse> doPoll(long durationInMillis) {
List<GetResponse> result = queues.stream()
.map(queue -> {
try {
return channel.basicGet(queue, false);
} catch (IOException e) {
log.error("Failed to get messages from queue: [{}]", queue);
throw new RuntimeException("Failed to get messages from queue.", e);
List<GetResponse> messages = new ArrayList<>();
for (int i = 0; i < maxPollMessages; i++) {
GetResponse response = doQueuePoll(queue);
if (response == null) {
break;
}
messages.add(response);
}
}).filter(Objects::nonNull).collect(Collectors.toList());
return messages;
})
.filter(r -> !r.isEmpty())
.flatMap(Collection::stream)
.collect(Collectors.toList());
if (result.size() > 0) {
return result;
} else {
@ -84,6 +93,15 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTb
}
}
protected GetResponse doQueuePoll(String queue) {
try {
return channel.basicGet(queue, false);
} catch (IOException e) {
log.error("Failed to get messages from queue: [{}]", queue);
throw new RuntimeException("Failed to get messages from queue.", e);
}
}
@Override
protected void doSubscribe(List<String> topicNames) {
queues = partitions.stream()

2
common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqSettings.java

@ -46,6 +46,8 @@ public class TbRabbitMqSettings {
private int connectionTimeout;
@Value("${queue.rabbitmq.handshake_timeout:}")
private int handshakeTimeout;
@Value("${queue.rabbitmq.max_poll_messages:1}")
private int maxPollMessages;
private ConnectionFactory connectionFactory;

19
common/queue/src/main/java/org/thingsboard/server/queue/scheduler/DefaultSchedulerComponent.java

@ -17,6 +17,7 @@ package org.thingsboard.server.queue.scheduler;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
@ -26,14 +27,15 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class DefaultSchedulerComponent implements SchedulerComponent {
protected ScheduledExecutorService schedulerExecutor;
private ScheduledExecutorService schedulerExecutor;
@PostConstruct
public void init() {
this.schedulerExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("queue-scheduler"));
schedulerExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("queue-scheduler"));
}
@PreDestroy
@ -52,10 +54,19 @@ public class DefaultSchedulerComponent implements SchedulerComponent {
}
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
return schedulerExecutor.scheduleAtFixedRate(command, initialDelay, period, unit);
return schedulerExecutor.scheduleAtFixedRate(() -> runSafely(command), initialDelay, period, unit);
}
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
return schedulerExecutor.scheduleWithFixedDelay(command, initialDelay, delay, unit);
return schedulerExecutor.scheduleWithFixedDelay(() -> runSafely(command), initialDelay, delay, unit);
}
private void runSafely(Runnable command) {
try {
command.run();
} catch (Throwable t) {
log.error("Unexpected error occurred while executing task!", t);
}
}
}

128
common/queue/src/test/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplateTest.java

@ -0,0 +1,128 @@
/**
* Copyright © 2016-2024 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.queue.rabbitmq;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.GetResponse;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueMsgDecoder;
import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
@ExtendWith(MockitoExtension.class)
class TbRabbitMqConsumerTemplateTest {
private static final String TOPIC = "some-topic";
@Mock
private TbQueueAdmin admin;
@Mock
private ConnectionFactory connectionFactory;
@Mock
private TbQueueMsgDecoder<DefaultTbQueueMsg> decoder;
@Mock
private Connection connection;
@Mock
private Channel channel;
@Mock
private TopicPartitionInfo partition;
@Mock
private GetResponse getResponse;
private TbRabbitMqConsumerTemplate<DefaultTbQueueMsg> consumer;
private void setUpConsumerWithMaxPollMessages(int maxPollMessages) throws Exception {
when(connectionFactory.newConnection()).thenReturn(connection);
when(connection.createChannel()).thenReturn(channel);
TbRabbitMqSettings settings = new TbRabbitMqSettings();
settings.setMaxPollMessages(maxPollMessages);
settings.setConnectionFactory(connectionFactory);
consumer = new TbRabbitMqConsumerTemplate<>(admin, settings, TOPIC, decoder);
when(partition.getFullTopicName()).thenReturn(TOPIC);
consumer.subscribe(Set.of(partition));
}
@Test
void pollWithMax5PollMessagesReturnsEmptyListIfNoMessages() throws Exception {
setUpConsumerWithMaxPollMessages(5);
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(null);
assertThat(consumer.poll(0L)).isEmpty();
verify(channel).basicGet(anyString(), anyBoolean());
}
@Test
void pollWithMax5PollMessagesReturns5MessagesIfQueueContains5() throws Exception {
setUpConsumerWithMaxPollMessages(5);
when(getResponse.getBody()).thenReturn(newMessageBody());
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(getResponse);
assertThat(consumer.poll(0L)).hasSize(5);
verify(channel, times(5)).basicGet(anyString(), anyBoolean());
}
@Test
void pollWithMax1PollMessageReturns1MessageIfQueueContainsMore() throws Exception {
setUpConsumerWithMaxPollMessages(1);
when(getResponse.getBody()).thenReturn(newMessageBody());
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(getResponse);
assertThat(consumer.poll(0L)).hasSize(1);
verify(channel).basicGet(anyString(), anyBoolean());
}
@Test
void pollWithMax3PollMessagesReturns2MessagesIfQueueContains2() throws Exception {
setUpConsumerWithMaxPollMessages(3);
when(getResponse.getBody()).thenReturn(newMessageBody());
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(getResponse, getResponse, null);
assertThat(consumer.poll(0L)).hasSize(2);
verify(channel, times(3)).basicGet(anyString(), anyBoolean());
}
private byte[] newMessageBody() {
return ("{\"key\": \"" + UUID.randomUUID() + "\"}").getBytes(StandardCharsets.UTF_8);
}
}

95
common/queue/src/test/java/org/thingsboard/server/queue/scheduler/DefaultSchedulerComponentTest.java

@ -0,0 +1,95 @@
/**
* Copyright © 2016-2024 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.queue.scheduler;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.assertj.core.api.Assertions.assertThat;
class DefaultSchedulerComponentTest {
DefaultSchedulerComponent schedulerComponent;
@BeforeEach
void setup() {
schedulerComponent = new DefaultSchedulerComponent();
schedulerComponent.init();
}
@AfterEach
void cleanup() {
schedulerComponent.destroy();
}
@Test
@DisplayName("scheduleAtFixedRate() should continue periodic execution even if command throws exception")
void scheduleAtFixedRateShouldNotStopPeriodicExecutionWhenCommandThrowsException() {
// GIVEN
var wasExecutedAtLeastOnce = new AtomicBoolean(false);
Runnable exceptionThrowingCommand = () -> {
try {
throw new RuntimeException("Unexpected exception");
} finally {
wasExecutedAtLeastOnce.set(true);
}
};
// WHEN
ScheduledFuture<?> future = schedulerComponent.scheduleAtFixedRate(exceptionThrowingCommand, 0, 200, TimeUnit.MILLISECONDS);
// THEN
Awaitility.await().alias("Wait until command is executed at least once")
.atMost(5, TimeUnit.SECONDS)
.until(wasExecutedAtLeastOnce::get);
assertThat(future.isDone()).as("Periodic execution should not stop after unhandled exception is thrown by the command").isFalse();
}
@Test
@DisplayName("scheduleWithFixedDelay() should continue periodic execution even if command throws exception")
void scheduleWithFixedDelayShouldNotStopPeriodicExecutionWhenCommandThrowsException() {
// GIVEN
var wasExecutedAtLeastOnce = new AtomicBoolean(false);
Runnable exceptionThrowingCommand = () -> {
try {
throw new RuntimeException("Unexpected exception");
} finally {
wasExecutedAtLeastOnce.set(true);
}
};
// WHEN
ScheduledFuture<?> future = schedulerComponent.scheduleWithFixedDelay(exceptionThrowingCommand, 0, 200, TimeUnit.MILLISECONDS);
// THEN
Awaitility.await().alias("Wait until command is executed at least once")
.atMost(5, TimeUnit.SECONDS)
.until(wasExecutedAtLeastOnce::get);
assertThat(future.isDone()).as("Periodic execution should not stop after unhandled exception is thrown by the command").isFalse();
}
}

4
common/transport/coap/src/main/proto/efento/proto_config.proto

@ -31,7 +31,7 @@ message ProtoOutputControlState {
uint32 channel_state = 2;
}
/* Message containing request data for accesing calibration parameters */
/* Message containing request data for accessing calibration parameters */
message ProtoCalibrationParameters {
/* Request details. Bitmask: */
@ -349,4 +349,4 @@ message ProtoConfig {
/* BLE advertising period configuration. */
ProtoBleAdvertisingPeriod ble_advertising_period = 59;
}
}

1
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -1063,6 +1063,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
log.debug("[{}][{}][{}] IOException: {}", sessionId,
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null),
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""),
cause.getMessage(),
cause);
} else if (log.isInfoEnabled()) {
log.info("[{}][{}][{}] IOException: {}", sessionId,

97
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/AbstractActivityManager.java

@ -69,7 +69,7 @@ public abstract class AbstractActivityManager<Key, Metadata> implements Activity
log.error("Failed to process activity event: provided activity key is null.");
return;
}
log.debug("Received activity event for key: [{}]", key);
log.debug("Received activity event for key: [{}]. Event time: [{}].", key, newLastRecordedTime);
var shouldReport = new AtomicBoolean(false);
var lastRecordedTime = new AtomicLong();
@ -94,7 +94,7 @@ public abstract class AbstractActivityManager<Key, Metadata> implements Activity
});
if (shouldReport.get() && lastReportedTime.get() < lastRecordedTime.get()) {
log.debug("Going to report first activity event for key: [{}].", key);
log.debug("Going to report first activity event for key: [{}]. Event time: [{}].", key, lastRecordedTime.get());
reportActivity(key, metadata, lastRecordedTime.get(), new ActivityReportCallback<>() {
@Override
public void onSuccess(Key key, long reportedTime) {
@ -103,7 +103,7 @@ public abstract class AbstractActivityManager<Key, Metadata> implements Activity
@Override
public void onFailure(Key key, Throwable t) {
log.debug("Failed to report first activity event for key: [{}].", key, t);
log.debug("Failed to report first activity event for key: [{}]. Event time: [{}].", key, lastRecordedTime.get(), t);
}
});
}
@ -113,50 +113,59 @@ public abstract class AbstractActivityManager<Key, Metadata> implements Activity
public void onReportingPeriodEnd() {
log.debug("Going to end reporting period.");
for (Map.Entry<Key, ActivityStateWrapper> entry : states.entrySet()) {
var key = entry.getKey();
var stateWrapper = entry.getValue();
var currentState = stateWrapper.getState();
long lastRecordedTime = currentState.getLastRecordedTime();
long lastReportedTime = stateWrapper.getLastReportedTime();
var metadata = currentState.getMetadata();
boolean hasExpired;
boolean shouldReport;
var updatedState = updateState(key, currentState);
if (updatedState != null) {
stateWrapper.setState(updatedState);
lastRecordedTime = updatedState.getLastRecordedTime();
metadata = updatedState.getMetadata();
hasExpired = hasExpired(lastRecordedTime);
shouldReport = stateWrapper.getStrategy().onReportingPeriodEnd();
} else {
states.remove(key);
hasExpired = false;
shouldReport = true;
Key key = entry.getKey();
ActivityStateWrapper stateWrapper = entry.getValue();
try {
reportLastEvent(key, stateWrapper);
} catch (Exception e) {
log.error("Failed to report last activity event on reporting period end for key: [{}]. State: [{}].", key, stateWrapper, e);
}
}
}
if (hasExpired) {
states.remove(key);
onStateExpiry(key, metadata);
shouldReport = true;
}
private void reportLastEvent(Key key, ActivityStateWrapper stateWrapper) {
var currentState = stateWrapper.getState();
long lastRecordedTime = currentState.getLastRecordedTime();
long lastReportedTime = stateWrapper.getLastReportedTime();
var metadata = currentState.getMetadata();
boolean hasExpired;
boolean shouldReport;
var updatedState = updateState(key, currentState);
if (updatedState != null) {
stateWrapper.setState(updatedState);
lastRecordedTime = updatedState.getLastRecordedTime();
metadata = updatedState.getMetadata();
hasExpired = hasExpired(lastRecordedTime);
shouldReport = stateWrapper.getStrategy().onReportingPeriodEnd();
} else {
states.remove(key);
hasExpired = false;
shouldReport = true;
}
if (shouldReport && lastReportedTime < lastRecordedTime) {
log.debug("Going to report last activity event for key: [{}].", key);
reportActivity(key, metadata, lastRecordedTime, new ActivityReportCallback<>() {
@Override
public void onSuccess(Key key, long reportedTime) {
updateLastReportedTime(key, reportedTime);
}
@Override
public void onFailure(Key key, Throwable t) {
log.debug("Failed to report last activity event for key: [{}].", key, t);
}
});
}
if (hasExpired) {
states.remove(key);
onStateExpiry(key, metadata);
shouldReport = true;
}
if (shouldReport && lastReportedTime < lastRecordedTime) {
long timeToReport = lastRecordedTime;
log.debug("Going to report last activity event for key: [{}]. Event time: [{}].", key, timeToReport);
reportActivity(key, metadata, timeToReport, new ActivityReportCallback<>() {
@Override
public void onSuccess(Key key, long reportedTime) {
updateLastReportedTime(key, reportedTime);
}
@Override
public void onFailure(Key key, Throwable t) {
log.debug("Failed to report last activity event for key: [{}]. Event time: [{}].", key, timeToReport, t);
}
});
}
}

2
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/FirstAndLastEventActivityStrategy.java

@ -16,7 +16,9 @@
package org.thingsboard.server.common.transport.activity.strategy;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ToString
@EqualsAndHashCode
public final class FirstAndLastEventActivityStrategy implements ActivityStrategy {

2
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/FirstEventActivityStrategy.java

@ -16,7 +16,9 @@
package org.thingsboard.server.common.transport.activity.strategy;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ToString
@EqualsAndHashCode
public final class FirstEventActivityStrategy implements ActivityStrategy {

10
common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java

@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.kv.KvEntry;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.Files;
@ -259,6 +260,15 @@ public class JacksonUtil {
}
}
public static JsonNode toJsonNode(InputStream value) {
try {
return value != null ? OBJECT_MAPPER.readTree(value) : null;
} catch (IOException e) {
throw new IllegalArgumentException("The given InputStream value: "
+ value + " cannot be transformed to a JsonNode", e);
}
}
public static ObjectNode newObjectNode() {
return newObjectNode(OBJECT_MAPPER);
}

9
dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCacheKey.java

@ -16,14 +16,14 @@
package org.thingsboard.server.dao.asset;
import lombok.Data;
import org.thingsboard.server.cache.VersionedCacheKey;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.io.Serializable;
@Data
public class AssetProfileCacheKey implements Serializable {
public class AssetProfileCacheKey implements VersionedCacheKey {
@Serial
private static final long serialVersionUID = 8220455917177676472L;
@ -63,4 +63,9 @@ public class AssetProfileCacheKey implements Serializable {
}
}
@Override
public boolean isVersioned() {
return assetProfileId != null;
}
}

7
dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java

@ -236,8 +236,9 @@ public class AssetProfileServiceImpl extends CachedVersionedEntityService<AssetP
log.trace("Executing findOrCreateAssetProfile");
AssetProfile assetProfile = findAssetProfileByName(tenantId, name, false);
if (assetProfile == null) {
boolean isDefault = "default".equals(name) && findDefaultAssetProfile(tenantId) == null;
try {
assetProfile = this.doCreateDefaultAssetProfile(tenantId, name, name.equals("default"), true);
assetProfile = this.doCreateAssetProfile(tenantId, name, isDefault, true);
} catch (DataValidationException e) {
if (ASSET_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS.equals(e.getMessage())) {
assetProfile = findAssetProfileByName(tenantId, name, false);
@ -252,10 +253,10 @@ public class AssetProfileServiceImpl extends CachedVersionedEntityService<AssetP
@Override
public AssetProfile createDefaultAssetProfile(TenantId tenantId) {
log.trace("Executing createDefaultAssetProfile tenantId [{}]", tenantId);
return doCreateDefaultAssetProfile(tenantId, "default", true, false);
return doCreateAssetProfile(tenantId, "default", true, false);
}
private AssetProfile doCreateDefaultAssetProfile(TenantId tenantId, String profileName, boolean defaultProfile, boolean publishSaveEvent) {
private AssetProfile doCreateAssetProfile(TenantId tenantId, String profileName, boolean defaultProfile, boolean publishSaveEvent) {
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
AssetProfile assetProfile = new AssetProfile();
assetProfile.setTenantId(tenantId);

9
dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCacheKey.java

@ -18,16 +18,16 @@ package org.thingsboard.server.dao.attributes;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.thingsboard.server.cache.VersionedCacheKey;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.id.EntityId;
import java.io.Serial;
import java.io.Serializable;
@EqualsAndHashCode
@Getter
@AllArgsConstructor
public class AttributeCacheKey implements Serializable {
public class AttributeCacheKey implements VersionedCacheKey {
@Serial
private static final long serialVersionUID = 2013369077925351881L;
@ -41,4 +41,9 @@ public class AttributeCacheKey implements Serializable {
return "{" + entityId + "}" + scope + "_" + key;
}
@Override
public boolean isVersioned() {
return true;
}
}

9
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java

@ -16,15 +16,15 @@
package org.thingsboard.server.dao.device;
import lombok.Data;
import org.thingsboard.server.cache.VersionedCacheKey;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.io.Serializable;
@Data
public class DeviceProfileCacheKey implements Serializable {
public class DeviceProfileCacheKey implements VersionedCacheKey {
@Serial
private static final long serialVersionUID = 8220455917177676472L;
@ -74,4 +74,9 @@ public class DeviceProfileCacheKey implements Serializable {
return tenantId + "_" + name;
}
@Override
public boolean isVersioned() {
return deviceProfileId != null;
}
}

7
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java

@ -271,8 +271,9 @@ public class DeviceProfileServiceImpl extends CachedVersionedEntityService<Devic
log.trace("Executing findOrCreateDefaultDeviceProfile");
DeviceProfile deviceProfile = findDeviceProfileByName(tenantId, name, false);
if (deviceProfile == null) {
boolean isDefault = "default".equals(name) && findDefaultDeviceProfile(tenantId) == null;
try {
deviceProfile = this.doCreateDefaultDeviceProfile(tenantId, name, name.equals("default"), true);
deviceProfile = this.doCreateDeviceProfile(tenantId, name, isDefault, true);
} catch (DataValidationException e) {
if (DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS.equals(e.getMessage())) {
deviceProfile = findDeviceProfileByName(tenantId, name, false);
@ -287,10 +288,10 @@ public class DeviceProfileServiceImpl extends CachedVersionedEntityService<Devic
@Override
public DeviceProfile createDefaultDeviceProfile(TenantId tenantId) {
log.trace("Executing createDefaultDeviceProfile tenantId [{}]", tenantId);
return doCreateDefaultDeviceProfile(tenantId, "default", true, false);
return doCreateDeviceProfile(tenantId, "default", true, false);
}
private DeviceProfile doCreateDefaultDeviceProfile(TenantId tenantId, String profileName, boolean defaultProfile, boolean publishSaveEvent) {
private DeviceProfile doCreateDeviceProfile(TenantId tenantId, String profileName, boolean defaultProfile, boolean publishSaveEvent) {
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setTenantId(tenantId);

4
dao/src/main/java/org/thingsboard/server/dao/edge/BaseRelatedEdgesService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.edge;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@ -30,6 +31,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
@Service
@Slf4j
public class BaseRelatedEdgesService extends AbstractCachedEntityService<RelatedEdgesCacheKey, RelatedEdgesCacheValue, RelatedEdgesEvictEvent> implements RelatedEdgesService {
public static final int RELATED_EDGES_CACHE_ITEMS = 1000;
@ -47,6 +49,7 @@ public class BaseRelatedEdgesService extends AbstractCachedEntityService<Related
@Override
public PageData<EdgeId> findEdgeIdsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) {
log.trace("Executing findEdgeIdsByEntityId, tenantId [{}], entityId [{}], pageLink [{}]", tenantId, entityId, pageLink);
if (!pageLink.equals(FIRST_PAGE)) {
return edgeService.findEdgeIdsByTenantIdAndEntityId(tenantId, entityId, pageLink);
}
@ -56,6 +59,7 @@ public class BaseRelatedEdgesService extends AbstractCachedEntityService<Related
@Override
public void publishRelatedEdgeIdsEvictEvent(TenantId tenantId, EntityId entityId) {
log.trace("Executing publishRelatedEdgeIdsEvictEvent, tenantId [{}], entityId [{}]", tenantId, entityId);
publishEvictEvent(new RelatedEdgesEvictEvent(tenantId, entityId));
}

3
dao/src/main/java/org/thingsboard/server/dao/entity/CachedVersionedEntityService.java

@ -16,12 +16,13 @@
package org.thingsboard.server.dao.entity;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.cache.VersionedCacheKey;
import org.thingsboard.server.cache.VersionedTbCache;
import org.thingsboard.server.common.data.HasVersion;
import java.io.Serializable;
public abstract class CachedVersionedEntityService<K extends Serializable, V extends Serializable & HasVersion, E> extends AbstractCachedEntityService<K, V, E> {
public abstract class CachedVersionedEntityService<K extends VersionedCacheKey, V extends Serializable & HasVersion, E> extends AbstractCachedEntityService<K, V, E> {
@Autowired
protected VersionedTbCache<K, V> cache;

9
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java

@ -18,17 +18,17 @@ package org.thingsboard.server.dao.entityview;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.thingsboard.server.cache.VersionedCacheKey;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.io.Serializable;
@Getter
@EqualsAndHashCode
@Builder
public class EntityViewCacheKey implements Serializable {
public class EntityViewCacheKey implements VersionedCacheKey {
@Serial
private static final long serialVersionUID = 5986277528222738163L;
@ -68,4 +68,9 @@ public class EntityViewCacheKey implements Serializable {
}
}
@Override
public boolean isVersioned() {
return entityViewId != null;
}
}

2
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -79,7 +79,9 @@ public class ModelConstants {
public static final String USER_CREDENTIALS_ENABLED_PROPERTY = "enabled";
public static final String USER_CREDENTIALS_PASSWORD_PROPERTY = "password"; //NOSONAR, the constant used to identify password column name (not password value itself)
public static final String USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY = "activate_token";
public static final String USER_CREDENTIALS_ACTIVATE_TOKEN_EXP_TIME_PROPERTY = "activate_token_exp_time";
public static final String USER_CREDENTIALS_RESET_TOKEN_PROPERTY = "reset_token";
public static final String USER_CREDENTIALS_RESET_TOKEN_EXP_TIME_PROPERTY = "reset_token_exp_time";
public static final String USER_CREDENTIALS_ADDITIONAL_PROPERTY = "additional_info";
/**

10
dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java

@ -50,9 +50,15 @@ public final class UserCredentialsEntity extends BaseSqlEntity<UserCredentials>
@Column(name = ModelConstants.USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY, unique = true)
private String activateToken;
@Column(name = ModelConstants.USER_CREDENTIALS_ACTIVATE_TOKEN_EXP_TIME_PROPERTY)
private Long activateTokenExpTime;
@Column(name = ModelConstants.USER_CREDENTIALS_RESET_TOKEN_PROPERTY, unique = true)
private String resetToken;
@Column(name = ModelConstants.USER_CREDENTIALS_RESET_TOKEN_EXP_TIME_PROPERTY)
private Long resetTokenExpTime;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.USER_CREDENTIALS_ADDITIONAL_PROPERTY)
private JsonNode additionalInfo;
@ -72,7 +78,9 @@ public final class UserCredentialsEntity extends BaseSqlEntity<UserCredentials>
this.enabled = userCredentials.isEnabled();
this.password = userCredentials.getPassword();
this.activateToken = userCredentials.getActivateToken();
this.activateTokenExpTime = userCredentials.getActivateTokenExpTime();
this.resetToken = userCredentials.getResetToken();
this.resetTokenExpTime = userCredentials.getResetTokenExpTime();
this.additionalInfo = userCredentials.getAdditionalInfo();
}
@ -86,7 +94,9 @@ public final class UserCredentialsEntity extends BaseSqlEntity<UserCredentials>
userCredentials.setEnabled(enabled);
userCredentials.setPassword(password);
userCredentials.setActivateToken(activateToken);
userCredentials.setActivateTokenExpTime(activateTokenExpTime);
userCredentials.setResetToken(resetToken);
userCredentials.setResetTokenExpTime(resetTokenExpTime);
userCredentials.setAdditionalInfo(additionalInfo);
return userCredentials;
}

6
dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java

@ -640,10 +640,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
log.warn("[{}] Failed to create ruleChain relation. Edge Id: [{}]", ruleChainId, edgeId);
throw new RuntimeException(e);
}
if (!ruleChainId.equals(edge.getRootRuleChainId())) {
eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).edgeId(edgeId).entityId(ruleChainId)
.actionType(ActionType.ASSIGNED_TO_EDGE).build());
}
eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).edgeId(edgeId).entityId(ruleChainId)
.actionType(ActionType.ASSIGNED_TO_EDGE).body(JacksonUtil.toString(edge)).build());
return ruleChain;
}

81
dao/src/main/java/org/thingsboard/server/dao/settings/DefaultSecuritySettingsService.java

@ -0,0 +1,81 @@
/**
* Copyright © 2016-2024 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.dao.settings;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.dao.service.ConstraintValidator;
import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE;
@Service
@RequiredArgsConstructor
public class DefaultSecuritySettingsService implements SecuritySettingsService {
private final AdminSettingsService adminSettingsService;
public static final int DEFAULT_MOBILE_SECRET_KEY_LENGTH = 64;
@Cacheable(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'")
@Override
public SecuritySettings getSecuritySettings() {
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "securitySettings");
SecuritySettings securitySettings;
if (adminSettings != null) {
try {
securitySettings = JacksonUtil.convertValue(adminSettings.getJsonValue(), SecuritySettings.class);
} catch (Exception e) {
throw new RuntimeException("Failed to load security settings!", e);
}
} else {
securitySettings = new SecuritySettings();
securitySettings.setPasswordPolicy(new UserPasswordPolicy());
securitySettings.getPasswordPolicy().setMinimumLength(6);
securitySettings.getPasswordPolicy().setMaximumLength(72);
securitySettings.setMobileSecretKeyLength(DEFAULT_MOBILE_SECRET_KEY_LENGTH);
securitySettings.setPasswordResetTokenTtl(24);
securitySettings.setUserActivationTokenTtl(24);
}
return securitySettings;
}
@CacheEvict(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'")
@Override
public SecuritySettings saveSecuritySettings(SecuritySettings securitySettings) {
ConstraintValidator.validateFields(securitySettings);
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "securitySettings");
if (adminSettings == null) {
adminSettings = new AdminSettings();
adminSettings.setTenantId(TenantId.SYS_TENANT_ID);
adminSettings.setKey("securitySettings");
}
adminSettings.setJsonValue(JacksonUtil.valueToTree(securitySettings));
AdminSettings savedAdminSettings = adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings);
try {
return JacksonUtil.convertValue(savedAdminSettings.getJsonValue(), SecuritySettings.class);
} catch (Exception e) {
throw new RuntimeException("Failed to load security settings!", e);
}
}
}

26
dao/src/main/java/org/thingsboard/server/dao/settings/SecuritySettingsService.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2024 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.dao.settings;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
public interface SecuritySettingsService {
SecuritySettings getSecuritySettings();
SecuritySettings saveSecuritySettings(SecuritySettings securitySettings);
}

9
dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestCacheKey.java

@ -18,15 +18,15 @@ package org.thingsboard.server.dao.timeseries;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.thingsboard.server.cache.VersionedCacheKey;
import org.thingsboard.server.common.data.id.EntityId;
import java.io.Serial;
import java.io.Serializable;
@EqualsAndHashCode
@Getter
@AllArgsConstructor
public class TsLatestCacheKey implements Serializable {
public class TsLatestCacheKey implements VersionedCacheKey {
@Serial
private static final long serialVersionUID = 2024369077925351881L;
@ -39,4 +39,9 @@ public class TsLatestCacheKey implements Serializable {
return "{" + entityId + "}" + key;
}
@Override
public boolean isVersioned() {
return true;
}
}

29
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -63,6 +63,7 @@ import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import java.util.ArrayList;
@ -72,6 +73,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.common.data.StringUtils.generateSafeToken;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -102,6 +104,7 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
private final UserAuthSettingsDao userAuthSettingsDao;
private final UserSettingsService userSettingsService;
private final UserSettingsDao userSettingsDao;
private final SecuritySettingsService securitySettingsService;
private final DataValidator<User> userValidator;
private final DataValidator<UserCredentials> userCredentialsValidator;
private final ApplicationEventPublisher eventPublisher;
@ -178,9 +181,9 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
countService.publishCountEntityEvictEvent(savedUser.getTenantId(), EntityType.USER);
UserCredentials userCredentials = new UserCredentials();
userCredentials.setEnabled(false);
userCredentials.setActivateToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
userCredentials.setUserId(new UserId(savedUser.getUuidId()));
userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode());
userCredentials = generateUserActivationToken(userCredentials);
userCredentialsDao.save(user.getTenantId(), userCredentials);
}
eventPublisher.publishEvent(SaveEntityEvent.builder()
@ -242,8 +245,12 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
if (userCredentials.isEnabled()) {
throw new IncorrectParameterException("User credentials already activated");
}
if (userCredentials.isActivationTokenExpired()) {
throw new IncorrectParameterException("Activation token expired");
}
userCredentials.setEnabled(true);
userCredentials.setActivateToken(null);
userCredentials.setActivateTokenExpTime(null);
userCredentials.setPassword(password);
if (userCredentials.getPassword() != null) {
updatePasswordHistory(userCredentials);
@ -263,7 +270,7 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
if (!userCredentials.isEnabled()) {
throw new DisabledException(String.format("User credentials not enabled [%s]", email));
}
userCredentials.setResetToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
userCredentials = generatePasswordResetToken(userCredentials);
return saveUserCredentials(tenantId, userCredentials);
}
@ -273,10 +280,26 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
if (!userCredentials.isEnabled()) {
throw new IncorrectParameterException("Unable to reset password for inactive user");
}
userCredentials.setResetToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
userCredentials = generatePasswordResetToken(userCredentials);
return saveUserCredentials(tenantId, userCredentials);
}
@Override
public UserCredentials generatePasswordResetToken(UserCredentials userCredentials) {
userCredentials.setResetToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
int ttlHours = securitySettingsService.getSecuritySettings().getPasswordResetTokenTtl();
userCredentials.setResetTokenExpTime(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(ttlHours));
return userCredentials;
}
@Override
public UserCredentials generateUserActivationToken(UserCredentials userCredentials) {
userCredentials.setActivateToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
int ttlHours = securitySettingsService.getSecuritySettings().getUserActivationTokenTtl();
userCredentials.setActivateTokenExpTime(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(ttlHours));
return userCredentials;
}
@Override
public UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials) {
log.trace("Executing replaceUserCredentials [{}]", userCredentials);

2
dao/src/main/resources/sql/schema-entities.sql

@ -491,9 +491,11 @@ CREATE TABLE IF NOT EXISTS user_credentials (
id uuid NOT NULL CONSTRAINT user_credentials_pkey PRIMARY KEY,
created_time bigint NOT NULL,
activate_token varchar(255) UNIQUE,
activate_token_exp_time BIGINT,
enabled boolean,
password varchar(255),
reset_token varchar(255) UNIQUE,
reset_token_exp_time BIGINT,
user_id uuid UNIQUE,
additional_info varchar DEFAULT '{}'
);

12
dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java

@ -163,6 +163,18 @@ public class AssetProfileServiceTest extends AbstractServiceTest {
});
}
@Test
public void testSaveAssetProfileWithNameDefaultAndProfileWithIsDefaultAndDifferentNameAlreadyExists() {
AssetProfile defaultAssetProfile = assetProfileService.findDefaultAssetProfile(tenantId);
Assert.assertNotNull(defaultAssetProfile);
defaultAssetProfile.setName("Asset Profile 1");
assetProfileService.saveAssetProfile(defaultAssetProfile);
AssetProfile createdAssetProfile = assetProfileService.findOrCreateAssetProfile(tenantId, "default");
Assert.assertNotNull(createdAssetProfile);
Assert.assertEquals("default", createdAssetProfile.getName());
Assert.assertFalse(createdAssetProfile.isDefault());
}
@Test
public void testDeleteAssetProfileWithExistingAsset() {
AssetProfile assetProfile = this.createAssetProfile(tenantId, "Asset Profile");

12
dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java

@ -209,6 +209,18 @@ public class DeviceProfileServiceTest extends AbstractServiceTest {
});
}
@Test
public void testSaveDeviceProfileWithNameDefaultAndProfileWithIsDefaultAndDifferentNameAlreadyExists() {
DeviceProfile defaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(tenantId);
Assert.assertNotNull(defaultDeviceProfile);
defaultDeviceProfile.setName("Device Profile 1");
deviceProfileService.saveDeviceProfile(defaultDeviceProfile);
DeviceProfile createdDeviceProfile = deviceProfileService.findOrCreateDeviceProfile(tenantId, "default");
Assert.assertNotNull(createdDeviceProfile);
Assert.assertEquals("default", createdDeviceProfile.getName());
Assert.assertFalse(createdDeviceProfile.isDefault());
}
@Ignore
@Test
public void testChangeDeviceProfileTypeWithExistingDevices() {

2
dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserCredentialsDaoTest.java

@ -63,7 +63,9 @@ public class JpaUserCredentialsDaoTest extends AbstractJpaDaoTest {
userCredentials.setUserId(new UserId(UUID.randomUUID()));
userCredentials.setPassword("password");
userCredentials.setActivateToken("ACTIVATE_TOKEN_" + number);
userCredentials.setActivateTokenExpTime(123L);
userCredentials.setResetToken("RESET_TOKEN_" + number);
userCredentials.setResetTokenExpTime(321L);
return userCredentialsDao.save(SYSTEM_TENANT_ID, userCredentials);
}

2
msa/js-executor/config/default.yml

@ -34,7 +34,7 @@ kafka:
partitions_consumed_concurrently: "1" # (EXPERIMENTAL) increase this value if you are planning to handle more than one partition (scale up, scale down) - this will decrease the latency
requestTimeout: "30000" # The default value in kafkajs is: 30000
connectionTimeout: "1000" # The default value in kafkajs is: 1000
compression: "gzip" # gzip or uncompressed
compression: "none" # gzip or uncompressed
topic_properties: "retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1"
use_confluent_cloud: false
client_id: "kafkajs" #inject pod name to easy identify the client using /opt/kafka/bin/kafka-consumer-groups.sh

26
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java

@ -15,10 +15,30 @@
*/
package org.thingsboard.mqtt;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.concurrent.Future;
import io.netty.buffer.ByteBuf;
public interface MqttHandler {
ListenableFuture<Void> onMessage(String topic, ByteBuf payload);
/**
* Changing ListenableFuture to Future allows you to choose CompletableFuture,
* which gives developers the freedom to choose the orchestration method.
* CompletableFuture is a newer, more evolved version that eliminates callback hell,
* is easier to use, and comes with the JDK. jdk 1.8 was previously used with the Before JDK1.8,
* use ListenableFuture, after that, it is recommended to use CompletableFuture.
* ListenableFuture It's still written that way.{@link MqttMessageListener#onMessage(topic, payload)}
* public ListenableFuture<Void> onMessage(String topic, ByteBuf message) {
* log.info("MQTT message [{}], topic [{}]", message.toString(StandardCharsets.UTF_8), topic);
* events.add(new MqttEvent(topic, message.toString(StandardCharsets.UTF_8)));
* return Futures.immediateVoidFuture();
* }
* CompletableFuture It's like this.
* public CompletableFuture<Void> onMessage(String topic, ByteBuf message) {
* log.info("MQTT message [{}], topic [{}]", message.toString(StandardCharsets.UTF_8), topic);
* events.add(new MqttEvent(topic, message.toString(StandardCharsets.UTF_8)));
* return CompletableFuture.completedFuture(null);
* }
* This change does not affect the system's current use of ListenableFuture so that it is free to choose between ListenableFuture or
* CompletableFuture in new development.
*/
Future<Void> onMessage(String topic, ByteBuf payload);
}

9
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -515,13 +515,16 @@ public class RestClient implements Closeable {
return restTemplate.postForEntity(baseURL + "/api/alarm", alarm, Alarm.class).getBody();
}
public List<EntitySubtype> getAlarmTypes(PageLink pageLink) {
public PageData<EntitySubtype> getAlarmTypes(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/alarm/types?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<EntitySubtype>>() {
}).getBody();
new ParameterizedTypeReference<PageData<EntitySubtype>>() {
},
params).getBody();
}
public AlarmComment saveAlarmComment(AlarmId alarmId, AlarmComment alarmComment) {

6
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java

@ -32,13 +32,13 @@ public interface MailService {
void sendTestMail(JsonNode config, String email) throws ThingsboardException;
void sendActivationEmail(String activationLink, String email) throws ThingsboardException;
void sendActivationEmail(String activationLink, long ttlMs, String email) throws ThingsboardException;
void sendAccountActivatedEmail(String loginLink, String email) throws ThingsboardException;
void sendResetPasswordEmail(String passwordResetLink, String email) throws ThingsboardException;
void sendResetPasswordEmail(String passwordResetLink, long ttlMs, String email) throws ThingsboardException;
void sendResetPasswordEmailAsync(String passwordResetLink, String email);
void sendResetPasswordEmailAsync(String passwordResetLink, long ttlMs, String email);
void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException;

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java

@ -135,7 +135,6 @@ public class TbHttpClient {
this.webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader(HttpHeaders.CONNECTION, "close") //In previous realization this header was present! (Added for hotfix "Connection reset")
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(
(config.getMaxInMemoryBufferSizeInKb() > 0 ? config.getMaxInMemoryBufferSizeInKb() : 256) * 1024))
.build();

50
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

9
ui-ngx/src/app/core/api/alias-controller.ts

@ -68,10 +68,11 @@ export class AliasController implements IAliasController {
private translate: TranslateService,
private stateControllerHolder: StateControllerHolder,
private origEntityAliases: EntityAliases,
private origFilters: Filters) {
private origFilters: Filters,
private origUserFilters?: Filters) {
this.entityAliases = deepClone(this.origEntityAliases) || {};
this.filters = deepClone(this.origFilters) || {};
this.userFilters = {};
this.userFilters = deepClone(this.origUserFilters) || {};
}
updateEntityAliases(newEntityAliases: EntityAliases) {
@ -166,6 +167,10 @@ export class AliasController implements IAliasController {
return this.filters;
}
getUserFilters(): Filters {
return this.userFilters;
}
getFilterInfo(filterId: string): FilterInfo {
if (this.userFilters[filterId]) {
return this.userFilters[filterId];

1
ui-ngx/src/app/core/api/widget-api.models.ts

@ -137,6 +137,7 @@ export interface IAliasController {
resolveAlarmSource(alarmSource: Datasource): Observable<Datasource>;
getEntityAliases(): EntityAliases;
getFilters(): Filters;
getUserFilters(): Filters;
getFilterInfo(filterId: string): FilterInfo;
getKeyFilters(filterId: string): Array<KeyFilter>;
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo): void;

2
ui-ngx/src/app/core/http/ota-package.service.ts

@ -59,7 +59,7 @@ export class OtaPackageService {
}
public getOtaPackage(otaPackageId: string, config?: RequestConfig): Observable<OtaPackage> {
return this.http.get<OtaPackage>(`/api/otaPackages/${otaPackageId}`, defaultHttpOptionsFromConfig(config));
return this.http.get<OtaPackage>(`/api/otaPackage/${otaPackageId}`, defaultHttpOptionsFromConfig(config));
}
public getOtaPackageInfo(otaPackageId: string, config?: RequestConfig): Observable<OtaPackageInfo> {

6
ui-ngx/src/app/core/http/user.service.ts

@ -16,7 +16,7 @@
import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { User, UserEmailInfo } from '@shared/models/user.model';
import { ActivationLinkInfo, User, UserEmailInfo } from '@shared/models/user.model';
import { Observable } from 'rxjs';
import { HttpClient, HttpParams } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
@ -77,6 +77,10 @@ export class UserService {
{...{responseType: 'text'}, ...defaultHttpOptionsFromConfig(config)});
}
public getActivationLinkInfo(userId: string, config?: RequestConfig): Observable<ActivationLinkInfo> {
return this.http.get<ActivationLinkInfo>(`/api/user/${userId}/activationLinkInfo`, defaultHttpOptionsFromConfig(config));
}
public sendActivationEmail(email: string, config?: RequestConfig) {
const encodeEmail = encodeURIComponent(email);
return this.http.post(`/api/user/sendActivationMail?email=${encodeEmail}`, null, defaultHttpOptionsFromConfig(config));

12
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -31,7 +31,7 @@ import {
DashboardLayoutsInfo,
DashboardState,
DashboardStateLayouts,
GridSettings,
GridSettings, LayoutType,
WidgetLayout
} from '@shared/models/dashboard.models';
import { deepClone, isDefined, isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined } from '@core/utils';
@ -406,6 +406,7 @@ export class DashboardUtilsService {
private createDefaultGridSettings(): GridSettings {
return {
layoutType: LayoutType.default,
backgroundColor: '#eeeeee',
columns: 24,
margin: 10,
@ -491,12 +492,15 @@ export class DashboardUtilsService {
if (!layout.gridSettings) {
layout.gridSettings = this.createDefaultGridSettings();
}
if (layout.gridSettings.margins && layout.gridSettings.margins.length === 2) {
layout.gridSettings.margin = layout.gridSettings.margins[0];
delete layout.gridSettings.margins;
if ((layout.gridSettings as any).margins && (layout.gridSettings as any).margins.length === 2) {
layout.gridSettings.margin = (layout.gridSettings as any).margins[0];
delete (layout.gridSettings as any).margins;
}
layout.gridSettings.outerMargin = isDefined(layout.gridSettings.outerMargin) ? layout.gridSettings.outerMargin : true;
layout.gridSettings.margin = isDefined(layout.gridSettings.margin) ? layout.gridSettings.margin : 10;
if (!layout.gridSettings.layoutType) {
layout.gridSettings.layoutType = LayoutType.default;
}
}
public setLayouts(dashboard: Dashboard, targetState: string, newLayouts: DashboardStateLayouts) {

100
ui-ngx/src/app/core/services/dynamic-component-factory.service.ts

@ -14,49 +14,17 @@
/// limitations under the License.
///
import {
Compiler,
Component,
Injectable,
Injector,
NgModule,
NgModuleRef,
OnDestroy,
Type,
ɵresetCompiledComponents
} from '@angular/core';
import { Component, Injectable, Type, ɵComponentDef, ɵNG_COMP_DEF } from '@angular/core';
import { from, Observable, of } from 'rxjs';
import { CommonModule } from '@angular/common';
import { mergeMap } from 'rxjs/operators';
@NgModule()
export abstract class DynamicComponentModule implements OnDestroy {
// eslint-disable-next-line @angular-eslint/contextual-lifecycle
ngOnDestroy(): void {
}
}
interface DynamicComponentData<T> {
componentType: Type<T>;
componentModuleRef: NgModuleRef<DynamicComponentModule>;
}
interface DynamicComponentModuleData {
moduleRef: NgModuleRef<DynamicComponentModule>;
moduleType: Type<DynamicComponentModule>;
}
@Injectable({
providedIn: 'root'
})
export class DynamicComponentFactoryService {
private dynamicComponentModulesMap = new Map<Type<any>, DynamicComponentModuleData>();
constructor(private compiler: Compiler,
private injector: Injector) {
constructor() {
}
public createDynamicComponent<T>(
@ -64,66 +32,38 @@ export class DynamicComponentFactoryService {
template: string,
modules?: Type<any>[],
preserveWhitespaces?: boolean,
compileAttempt = 1,
styles?: string[]): Observable<DynamicComponentData<T>> {
styles?: string[]): Observable<Type<T>> {
return from(import('@angular/compiler')).pipe(
mergeMap(() => {
const comp = this._createDynamicComponent(componentType, template, preserveWhitespaces, styles);
let moduleImports: Type<any>[] = [CommonModule];
let componentImports: Type<any>[] = [CommonModule];
if (modules) {
moduleImports = [...moduleImports, ...modules];
}
// noinspection AngularInvalidImportedOrDeclaredSymbol
const dynamicComponentInstanceModule = NgModule({
declarations: [comp],
imports: moduleImports
})(class DynamicComponentInstanceModule extends DynamicComponentModule {});
try {
const module = this.compiler.compileModuleSync(dynamicComponentInstanceModule);
let moduleRef: NgModuleRef<any>;
try {
moduleRef = module.create(this.injector);
} catch (e) {
this.compiler.clearCacheFor(module.moduleType);
throw e;
}
this.dynamicComponentModulesMap.set(comp, {
moduleRef,
moduleType: module.moduleType
});
return of( {
componentType: comp,
componentModuleRef: moduleRef
});
} catch (error) {
if (compileAttempt === 1) {
ɵresetCompiledComponents();
return this.createDynamicComponent(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles);
} else {
console.error(error);
throw error;
}
componentImports = [...componentImports, ...modules];
}
const comp = this.createAndCompileDynamicComponent(componentType, template, componentImports, preserveWhitespaces, styles);
return of(comp.type);
})
);
}
public destroyDynamicComponent<T>(componentType: Type<T>) {
const moduleData = this.dynamicComponentModulesMap.get(componentType);
if (moduleData) {
moduleData.moduleRef.destroy();
this.compiler.clearCacheFor(moduleData.moduleType);
this.dynamicComponentModulesMap.delete(componentType);
}
public destroyDynamicComponent<T>(_componentType: Type<T>) {
}
public getComponentDef<T>(componentType: Type<T>): ɵComponentDef<T> {
return componentType[ɵNG_COMP_DEF];
}
private _createDynamicComponent<T>(componentType: Type<T>, template: string, preserveWhitespaces?: boolean, styles?: string[]): Type<T> {
private createAndCompileDynamicComponent<T>(componentType: Type<T>, template: string, imports: Type<any>[],
preserveWhitespaces?: boolean, styles?: string[]): ɵComponentDef<T> {
// noinspection AngularMissingOrInvalidDeclarationInModule
return Component({
const comp = Component({
template,
imports,
preserveWhitespaces,
styles
styles,
standalone: true
})(componentType);
// Trigger component compilation
return comp[ɵNG_COMP_DEF];
}
}

71
ui-ngx/src/app/core/services/resources.service.ts

@ -21,7 +21,7 @@ import {
Injectable,
Injector,
ModuleWithComponentFactories,
Type
Type, ɵNG_MOD_DEF
} from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs';
@ -50,7 +50,6 @@ export class ResourcesService {
private loadedJsonResources: { [url: string]: ReplaySubject<any> } = {};
private loadedResources: { [url: string]: ReplaySubject<void> } = {};
private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {};
private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {};
private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0];
@ -201,71 +200,6 @@ export class ResourcesService {
);
}
public loadModules(resourceId: string | TbResourceId, modulesMap: IModulesMap): Observable<Type<any>[]> {
const url = this.getDownloadUrl(resourceId);
if (this.loadedModules[url]) {
return this.loadedModules[url].asObservable();
}
modulesMap.init();
const meta = this.getMetaInfo(resourceId);
const subject = new ReplaySubject<Type<any>[]>();
this.loadedModules[url] = subject;
import('@angular/compiler').then(
() => {
System.import(url, undefined, meta).then(
(module) => {
try {
let modules;
try {
modules = this.extractNgModules(module);
} catch (e) {
console.error(e);
}
if (modules && modules.length) {
const tasks: Promise<ModuleWithComponentFactories<any>>[] = [];
for (const m of modules) {
tasks.push(this.compiler.compileModuleAndAllComponentsAsync(m));
}
forkJoin(tasks).subscribe((compiled) => {
try {
for (const c of compiled) {
c.ngModuleFactory.create(this.injector);
}
this.loadedModules[url].next(modules);
this.loadedModules[url].complete();
} catch (e) {
this.loadedModules[url].error(new Error(`Unable to init module from url: ${url}`));
}
},
(e) => {
this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`));
});
} else {
this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export or not NgModule!`));
}
} catch (e) {
this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`));
}
},
(e) => {
this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`));
console.error(`Unable to load module from url: ${url}`, e);
}
);
}
);
return subject.asObservable().pipe(
tap({
next: () => System.delete(url),
error: () => {
delete this.loadedModulesAndFactories[url];
System.delete(url);
},
complete: () => System.delete(url)
})
);
}
private extractNgModules(module: any, modules: Type<any>[] = []): Type<any>[] {
try {
let potentialModules = [module];
@ -274,7 +208,7 @@ export class ResourcesService {
while (potentialModules.length && currentScanDepth < 10) {
const newPotentialModules = [];
for (const potentialModule of potentialModules) {
if (potentialModule && ('ɵmod' in potentialModule)) {
if (potentialModule && (ɵNG_MOD_DEF in potentialModule)) {
modules.push(potentialModule);
} else {
for (const k of Object.keys(potentialModule)) {
@ -349,7 +283,6 @@ export class ResourcesService {
}
private clearModulesCache() {
this.loadedModules = {};
this.loadedModulesAndFactories = {};
}
}

6
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -522,7 +522,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.translate,
() => this.dashboardCtx.stateController,
this.dashboardConfiguration.entityAliases,
this.dashboardConfiguration.filters);
this.dashboardConfiguration.filters,
this.parentDashboard?.aliasController.getUserFilters());
this.updateDashboardCss();
@ -1168,8 +1169,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.filtersUpdated();
this.updateLayouts();
}
} else if (!this.widgetEditMode) {
this.dashboard.configuration.timewindow = this.dashboardCtx.dashboardTimewindow;
}
}
}
@ -1216,6 +1215,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.setEditMode(false, false);
} else {
let reInitDashboard = false;
this.dashboard.configuration.timewindow = this.dashboardCtx.dashboardTimewindow;
this.dashboardService.saveDashboard(this.dashboard).pipe(
catchError((err) => {
if (err.status === HttpStatusCode.Conflict) {

85
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html

@ -119,20 +119,15 @@
</div>
<div class="tb-form-row space-between">
<div translate>dashboard.columns-count</div>
<mat-form-field *ngIf="!isScada()"
appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<mat-form-field *ngIf="!isScada()" appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('columns').invalid && gridSettingsFormGroup.get('columns').touched)"
[matTooltip]="(gridSettingsFormGroup.get('columns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('columns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate">
<input matInput formControlName="columns" type="number" min="10" max="1008" step="1"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('columns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('columns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate"
*ngIf="gridSettingsFormGroup.get('columns').invalid && gridSettingsFormGroup.get('columns').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
<mat-form-field *ngIf="isScada()"
class="medium-width" appearance="outline" subscriptSizing="dynamic">
@ -145,37 +140,29 @@
</div>
<div class="tb-form-row space-between">
<div translate>dashboard.min-layout-width</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('minColumns').invalid && gridSettingsFormGroup.get('minColumns').touched)"
[matTooltip]="(gridSettingsFormGroup.get('minColumns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('minColumns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate">
<input matInput formControlName="minColumns" type="number" min="10" max="1008" step="1"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('minColumns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('minColumns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate"
*ngIf="gridSettingsFormGroup.get('minColumns').invalid && gridSettingsFormGroup.get('minColumns').touched"
class="tb-error">
warning
</mat-icon>
<span translate matSuffix>dashboard.columns-suffix</span>
</mat-form-field>
</div>
<div *ngIf="!isScada()" class="tb-form-row space-between">
<div translate>dashboard.widgets-margins</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('margin').invalid && gridSettingsFormGroup.get('margin').touched)"
[matTooltip]="(gridSettingsFormGroup.get('margin').hasError('required') ? 'dashboard.margin-required' :
(gridSettingsFormGroup.get('margin').hasError('min') ? 'dashboard.min-margin-message' : 'dashboard.max-margin-message')) | translate">
<input matInput formControlName="margin" type="number" min="0" max="50" step="any"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('margin').hasError('required') ? 'dashboard.margin-required' :
(gridSettingsFormGroup.get('margin').hasError('min') ? 'dashboard.min-margin-message' : 'dashboard.max-margin-message')) | translate"
*ngIf="gridSettingsFormGroup.get('margin').invalid && gridSettingsFormGroup.get('margin').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div *ngIf="!isScada()" class="tb-form-row">
@ -190,20 +177,16 @@
</div>
<div *ngIf="isDefaultLayout && !isDefaultBreakpoint" class="tb-form-row space-between">
<div translate>dashboard.row-height</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('rowHeight').invalid && gridSettingsFormGroup.get('rowHeight').touched)"
[matTooltip]="(gridSettingsFormGroup.get('rowHeight').hasError('required') ? 'dashboard.row-height-required' :
(gridSettingsFormGroup.get('rowHeight').hasError('min') ?
'dashboard.min-row-height-message' : 'dashboard.max-row-height-message')) | translate">
<input matInput formControlName="rowHeight" type="number" min="5" max="200" step="any"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('rowHeight').hasError('required') ? 'dashboard.row-height-required' :
(gridSettingsFormGroup.get('rowHeight').hasError('min') ?
'dashboard.min-row-height-message' : 'dashboard.max-row-height-message')) | translate"
*ngIf="gridSettingsFormGroup.get('rowHeight').invalid && gridSettingsFormGroup.get('rowHeight').touched"
class="tb-error">
warning
</mat-icon>
<span matSuffix>px</span>
</mat-form-field>
</div>
@ -245,20 +228,16 @@
</div>
<div *ngIf="!isScada()" class="tb-form-row space-between">
<div translate>dashboard.mobile-row-height</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('mobileRowHeight').invalid && gridSettingsFormGroup.get('mobileRowHeight').touched)"
[matTooltip]="(gridSettingsFormGroup.get('mobileRowHeight').hasError('required') ? 'dashboard.mobile-row-height-required' :
(gridSettingsFormGroup.get('mobileRowHeight').hasError('min') ?
'dashboard.min-mobile-row-height-message' : 'dashboard.max-mobile-row-height-message')) | translate">
<input matInput formControlName="mobileRowHeight" type="number" min="5" max="200" step="any"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('mobileRowHeight').hasError('required') ? 'dashboard.mobile-row-height-required' :
(gridSettingsFormGroup.get('mobileRowHeight').hasError('min') ?
'dashboard.min-mobile-row-height-message' : 'dashboard.max-mobile-row-height-message')) | translate"
*ngIf="gridSettingsFormGroup.get('mobileRowHeight').invalid && gridSettingsFormGroup.get('mobileRowHeight').touched"
class="tb-error">
warning
</mat-icon>
<span matSuffix>px</span>
</mat-form-field>
</div>

6
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html

@ -126,6 +126,12 @@
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-padding' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">

2
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts

@ -144,6 +144,7 @@ export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
padding: [settings.padding, []],
actions: [configData.config.actions || {}, []]
});
@ -183,6 +184,7 @@ export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.settings.padding = config.padding;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;

6
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html

@ -94,6 +94,12 @@
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-padding' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">

2
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts

@ -79,6 +79,7 @@ export class StatusWidgetBasicConfigComponent extends BasicWidgetConfigComponent
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
padding: [settings.padding, []],
actions: [configData.config.actions || {}, []]
});
@ -99,6 +100,7 @@ export class StatusWidgetBasicConfigComponent extends BasicWidgetConfigComponent
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.settings.padding = config.padding;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;

6
ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html

@ -211,6 +211,12 @@
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-padding' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between column-lt-md">
<div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save