Browse Source

Merge branch 'master' into fix/3969-lw2m2m-resource-read-only

pull/11632/head
Max Petrov 2 years ago
committed by GitHub
parent
commit
f3d3540662
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      application/src/main/data/json/system/scada_symbols/sand-filter.svg
  2. 74
      application/src/main/data/json/system/widget_bundles/scada_fluid_system.json
  3. 2
      application/src/main/data/json/system/widget_bundles/scada_symbols.json
  4. 74
      application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json
  5. 2
      application/src/main/data/json/system/widget_types/scada_symbol.json
  6. 19
      application/src/main/data/upgrade/3.7.0/schema_update.sql
  7. 6
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  8. 126
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  9. 25
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  10. 2
      application/src/main/java/org/thingsboard/server/controller/ImageController.java
  11. 3
      application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java
  12. 1
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  13. 69
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  14. 13
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  15. 2
      application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java
  16. 32
      application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java
  17. 5
      application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java
  18. 1
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
  19. 11
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  20. 2
      application/src/main/java/org/thingsboard/server/service/mail/DefaultTbMailConfigTemplateService.java
  21. 10
      application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java
  22. 8
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  23. 82
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  24. 6
      application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java
  25. 10
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  26. 2
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  27. 2
      application/src/main/resources/templates/activation.ftl
  28. 2
      application/src/main/resources/templates/reset.password.ftl
  29. 5
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  30. 184
      application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java
  31. 25
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  32. 18
      application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
  33. 12
      application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java
  34. 5
      common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java
  35. 2
      common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java
  36. 16
      common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java
  37. 8
      common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java
  38. 26
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedCacheKey.java
  39. 2
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java
  40. 19
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java
  41. 2
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbCache.java
  42. 9
      common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java
  43. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  44. 19
      common/data/src/main/java/org/thingsboard/server/common/data/UserActivationLink.java
  45. 104
      common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java
  46. 25
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java
  47. 10
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  48. 9
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCacheKey.java
  49. 9
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCacheKey.java
  50. 9
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java
  51. 4
      dao/src/main/java/org/thingsboard/server/dao/edge/BaseRelatedEdgesService.java
  52. 3
      dao/src/main/java/org/thingsboard/server/dao/entity/CachedVersionedEntityService.java
  53. 9
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java
  54. 2
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  55. 10
      dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java
  56. 6
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  57. 81
      dao/src/main/java/org/thingsboard/server/dao/settings/DefaultSecuritySettingsService.java
  58. 26
      dao/src/main/java/org/thingsboard/server/dao/settings/SecuritySettingsService.java
  59. 9
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestCacheKey.java
  60. 29
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  61. 2
      dao/src/main/resources/sql/schema-entities.sql
  62. 2
      dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserCredentialsDaoTest.java
  63. 6
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java
  64. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
  65. 6
      ui-ngx/src/app/core/http/user.service.ts
  66. 100
      ui-ngx/src/app/core/services/dynamic-component-factory.service.ts
  67. 71
      ui-ngx/src/app/core/services/resources.service.ts
  68. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html
  69. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts
  70. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html
  71. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts
  72. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html
  73. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.ts
  74. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html
  75. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.ts
  76. 13
      ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts
  77. 7
      ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts
  78. 54
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html
  79. 13
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts
  80. 10
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts
  81. 2
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html
  82. 25
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts
  83. 4
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.models.ts
  84. 2
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html
  85. 13
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts
  86. 4
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.models.ts
  87. 18
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts
  88. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html
  89. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.ts
  90. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html
  91. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts
  92. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.html
  93. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.ts
  94. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html
  95. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.ts
  96. 2
      ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.html
  97. 6
      ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.ts
  98. 5
      ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.models.ts
  99. 5
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  100. 2
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts

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"> <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[{ <tb:metadata xmlns=""><![CDATA[{
"title": "Stand filter", "title": "Sand filter",
"description": "Stand filter with configurable filtration mode option and various states.", "description": "Sand filter with configurable filtration mode option and various states.",
"searchTags": [ "searchTags": [
"filter" "filter",
"sand"
], ],
"widgetSizeX": 3, "widgetSizeX": 3,
"widgetSizeY": 5, "widgetSizeY": 5,

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 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 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.config.annotations.ApiOperation;
import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.settings.AdminSettingsService; 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.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.service.security.auth.oauth2.CookieUtils; import org.thingsboard.server.service.security.auth.oauth2.CookieUtils;
@ -109,6 +110,7 @@ public class AdminController extends BaseController {
private final SmsService smsService; private final SmsService smsService;
private final AdminSettingsService adminSettingsService; private final AdminSettingsService adminSettingsService;
private final SystemSecurityService systemSecurityService; private final SystemSecurityService systemSecurityService;
private final SecuritySettingsService securitySettingsService;
private final JwtSettingsService jwtSettingsService; private final JwtSettingsService jwtSettingsService;
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
private final EntitiesVersionControlService versionControlService; private final EntitiesVersionControlService versionControlService;
@ -167,7 +169,7 @@ public class AdminController extends BaseController {
@ResponseBody @ResponseBody
public SecuritySettings getSecuritySettings() throws ThingsboardException { public SecuritySettings getSecuritySettings() throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return checkNotNull(systemSecurityService.getSecuritySettings()); return checkNotNull(securitySettingsService.getSecuritySettings());
} }
@ApiOperation(value = "Update Security Settings (saveSecuritySettings)", @ApiOperation(value = "Update Security Settings (saveSecuritySettings)",
@ -179,7 +181,7 @@ public class AdminController extends BaseController {
@Parameter(description = "A JSON value representing the Security Settings.") @Parameter(description = "A JSON value representing the Security Settings.")
@RequestBody SecuritySettings securitySettings) throws ThingsboardException { @RequestBody SecuritySettings securitySettings) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
securitySettings = checkNotNull(systemSecurityService.saveSecuritySettings(securitySettings)); securitySettings = checkNotNull(securitySettingsService.saveSecuritySettings(securitySettings));
return 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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; 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.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.cache.limits.RateLimitService; 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.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.config.annotations.ApiOperation; 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.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.ActivateUserRequest; 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.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.net.URI;
import java.net.URISyntaxException;
@RestController @RestController
@TbCoreComponent @TbCoreComponent
@RequestMapping("/api") @RequestMapping("/api")
@ -75,6 +71,7 @@ public class AuthController extends BaseController {
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
private final MailService mailService; private final MailService mailService;
private final SystemSecurityService systemSecurityService; private final SystemSecurityService systemSecurityService;
private final SecuritySettingsService securitySettingsService;
private final RateLimitService rateLimitService; private final RateLimitService rateLimitService;
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
@ -82,9 +79,8 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Get current User (getUser)", @ApiOperation(value = "Get current User (getUser)",
notes = "Get the information about the User which credentials are used to perform this REST API call.") 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')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/auth/user", method = RequestMethod.GET) @GetMapping(value = "/auth/user")
public @ResponseBody public User getUser() throws ThingsboardException {
User getUser() throws ThingsboardException {
SecurityUser securityUser = getCurrentUser(); SecurityUser securityUser = getCurrentUser();
return userService.findUserById(securityUser.getTenantId(), securityUser.getId()); return userService.findUserById(securityUser.getTenantId(), securityUser.getId());
} }
@ -92,8 +88,7 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Logout (logout)", @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. ") 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')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/auth/logout", method = RequestMethod.POST) @PostMapping(value = "/auth/logout")
@ResponseStatus(value = HttpStatus.OK)
public void logout(HttpServletRequest request) throws ThingsboardException { public void logout(HttpServletRequest request) throws ThingsboardException {
logLogoutAction(request); logLogoutAction(request);
} }
@ -101,8 +96,7 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Change password for current User (changePassword)", @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.") 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')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST) @PostMapping(value = "/auth/changePassword")
@ResponseStatus(value = HttpStatus.OK)
public JwtPair changePassword(@Parameter(description = "Change Password Request") public JwtPair changePassword(@Parameter(description = "Change Password Request")
@RequestBody ChangePasswordRequest changePasswordRequest) throws ThingsboardException { @RequestBody ChangePasswordRequest changePasswordRequest) throws ThingsboardException {
String currentPassword = changePasswordRequest.getCurrentPassword(); String currentPassword = changePasswordRequest.getCurrentPassword();
@ -125,46 +119,34 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Get the current User password policy (getUserPasswordPolicy)", @ApiOperation(value = "Get the current User password policy (getUserPasswordPolicy)",
notes = "API call to get the password policy for the password validation form(s).") notes = "API call to get the password policy for the password validation form(s).")
@RequestMapping(value = "/noauth/userPasswordPolicy", method = RequestMethod.GET) @GetMapping(value = "/noauth/userPasswordPolicy")
@ResponseBody
public UserPasswordPolicy getUserPasswordPolicy() throws ThingsboardException { public UserPasswordPolicy getUserPasswordPolicy() throws ThingsboardException {
SecuritySettings securitySettings = SecuritySettings securitySettings = checkNotNull(securitySettingsService.getSecuritySettings());
checkNotNull(systemSecurityService.getSecuritySettings());
return securitySettings.getPasswordPolicy(); return securitySettings.getPasswordPolicy();
} }
@ApiOperation(value = "Check Activate User Token (checkActivateToken)", @ApiOperation(value = "Check Activate User Token (checkActivateToken)",
notes = "Checks the activation token and forwards user to 'Create Password' page. " + 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 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'.") "If token is not valid, returns '409 Conflict'. " +
@RequestMapping(value = "/noauth/activate", params = {"activateToken"}, method = RequestMethod.GET) "If token is expired, redirects to error page.")
public ResponseEntity<String> checkActivateToken( @GetMapping(value = "/noauth/activate", params = {"activateToken"})
public ResponseEntity<?> checkActivateToken(
@Parameter(description = "The activate token string.") @Parameter(description = "The activate token string.")
@RequestParam(value = "activateToken") String activateToken) { @RequestParam(value = "activateToken") String activateToken) {
HttpHeaders headers = new HttpHeaders();
HttpStatus responseStatus;
UserCredentials userCredentials = userService.findUserCredentialsByActivateToken(TenantId.SYS_TENANT_ID, activateToken); UserCredentials userCredentials = userService.findUserCredentialsByActivateToken(TenantId.SYS_TENANT_ID, activateToken);
if (userCredentials != null) { if (userCredentials == null) {
String createURI = "/login/createPassword"; return response(HttpStatus.CONFLICT);
try { } else if (userCredentials.isActivationTokenExpired()) {
URI location = new URI(createURI + "?activateToken=" + activateToken); return redirectTo("/activationLinkExpired");
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;
} }
return new ResponseEntity<>(headers, responseStatus); return redirectTo("/login/createPassword?activateToken=" + activateToken);
} }
@ApiOperation(value = "Request reset password email (requestResetPasswordByEmail)", @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. " + 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.") "Always return '200 OK' status for security purposes.")
@RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST) @PostMapping(value = "/noauth/resetPasswordByEmail")
@ResponseStatus(value = HttpStatus.OK)
public void requestResetPasswordByEmail( public void requestResetPasswordByEmail(
@Parameter(description = "The JSON object representing the reset password email request.") @Parameter(description = "The JSON object representing the reset password email request.")
@RequestBody ResetPasswordEmailRequest resetPasswordByEmailRequest, @RequestBody ResetPasswordEmailRequest resetPasswordByEmailRequest,
@ -177,7 +159,7 @@ public class AuthController extends BaseController {
String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl, String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
userCredentials.getResetToken()); userCredentials.getResetToken());
mailService.sendResetPasswordEmailAsync(resetUrl, email); mailService.sendResetPasswordEmailAsync(resetUrl, userCredentials.getResetTokenTtl(), email);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error occurred: {}", e.getMessage()); log.warn("Error occurred: {}", e.getMessage());
} }
@ -186,32 +168,22 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Check password reset token (checkResetToken)", @ApiOperation(value = "Check password reset token (checkResetToken)",
notes = "Checks the password reset token and forwards user to 'Reset Password' page. " + 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 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'.") "If token is not valid, returns '409 Conflict'. " +
@RequestMapping(value = "/noauth/resetPassword", params = {"resetToken"}, method = RequestMethod.GET) "If token is expired, redirects to error page.")
public ResponseEntity<String> checkResetToken( @GetMapping(value = "/noauth/resetPassword", params = {"resetToken"})
public ResponseEntity<?> checkResetToken(
@Parameter(description = "The reset token string.") @Parameter(description = "The reset token string.")
@RequestParam(value = "resetToken") String resetToken) { @RequestParam(value = "resetToken") String resetToken) {
HttpHeaders headers = new HttpHeaders();
HttpStatus responseStatus;
String resetURI = "/login/resetPassword";
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken); UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
if (userCredentials == null) {
if (userCredentials != null) { return response(HttpStatus.CONFLICT);
if (!rateLimitService.checkRateLimit(LimitedApi.PASSWORD_RESET, userCredentials.getUserId(), defaultLimitsConfiguration)) { } else if (userCredentials.isResetTokenExpired()) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build(); return redirectTo("/passwordResetLinkExpired");
} }
try { if (!rateLimitService.checkRateLimit(LimitedApi.PASSWORD_RESET, userCredentials.getUserId(), defaultLimitsConfiguration)) {
URI location = new URI(resetURI + "?resetToken=" + resetToken); return response(HttpStatus.TOO_MANY_REQUESTS);
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;
} }
return new ResponseEntity<>(headers, responseStatus); return redirectTo("/login/resetPassword?resetToken=" + resetToken);
} }
@ApiOperation(value = "Activate User", @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, " + "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. " + "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 valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " +
"If token is not valid, returns '404 Bad Request'.") "If token is not valid, returns '400 Bad Request'.")
@RequestMapping(value = "/noauth/activate", method = RequestMethod.POST) @PostMapping(value = "/noauth/activate")
@ResponseStatus(value = HttpStatus.OK) public JwtPair activateUser(@Parameter(description = "Activate user request.")
@ResponseBody @RequestBody ActivateUserRequest activateRequest,
public JwtPair activateUser( @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
@Parameter(description = "Activate user request.") HttpServletRequest request) {
@RequestBody ActivateUserRequest activateRequest,
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
HttpServletRequest request) throws ThingsboardException {
String activateToken = activateRequest.getActivateToken(); String activateToken = activateRequest.getActivateToken();
String password = activateRequest.getPassword(); String password = activateRequest.getPassword();
systemSecurityService.validatePassword(password, null); systemSecurityService.validatePassword(password, null);
@ -258,18 +227,18 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Reset password (resetPassword)", @ApiOperation(value = "Reset password (resetPassword)",
notes = "Checks the password reset token and updates the password. " + 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 valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " +
"If token is not valid, returns '404 Bad Request'.") "If token is not valid, returns '400 Bad Request'.")
@RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST) @PostMapping(value = "/noauth/resetPassword")
@ResponseStatus(value = HttpStatus.OK) public JwtPair resetPassword(@Parameter(description = "Reset password request.")
@ResponseBody @RequestBody ResetPasswordRequest resetPasswordRequest,
public JwtPair resetPassword( HttpServletRequest request) throws ThingsboardException {
@Parameter(description = "Reset password request.")
@RequestBody ResetPasswordRequest resetPasswordRequest,
HttpServletRequest request) throws ThingsboardException {
String resetToken = resetPasswordRequest.getResetToken(); String resetToken = resetPasswordRequest.getResetToken();
String password = resetPasswordRequest.getPassword(); String password = resetPasswordRequest.getPassword();
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken); UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
if (userCredentials != null) { if (userCredentials != null) {
if (userCredentials.isResetTokenExpired()) {
throw new ThingsboardException("Password reset token expired", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
systemSecurityService.validatePassword(password, userCredentials); systemSecurityService.validatePassword(password, userCredentials);
if (passwordEncoder.matches(password, userCredentials.getPassword())) { if (passwordEncoder.matches(password, userCredentials.getPassword())) {
throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); 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); String encodedPassword = passwordEncoder.encode(password);
userCredentials.setPassword(encodedPassword); userCredentials.setPassword(encodedPassword);
userCredentials.setResetToken(null); userCredentials.setResetToken(null);
userCredentials.setResetTokenExpTime(null);
userCredentials = userService.replaceUserCredentials(TenantId.SYS_TENANT_ID, userCredentials); userCredentials = userService.replaceUserCredentials(TenantId.SYS_TENANT_ID, userCredentials);
User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId()); User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId());
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); 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.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.MethodArgumentNotValidException; 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.exception.IncorrectParameterException;
import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.model.ModelConstants; 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.OAuth2ClientService;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService; 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.AlarmSubscriptionService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -191,7 +193,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@TbCoreComponent @TbCoreComponent
public abstract class BaseController { 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*/ /*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 { protected List<OAuth2ClientId> getOAuth2ClientIds(UUID[] ids) throws ThingsboardException {
if (ids == null) { if (ids == null) {
return Collections.emptyList(); 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)) { if (StringUtils.isNotEmpty(etag)) {
etag = StringUtils.remove(etag, '\"'); // etag is wrapped in double quotes due to HTTP specification etag = StringUtils.remove(etag, '\"'); // etag is wrapped in double quotes due to HTTP specification
if (etag.equals(tbImageService.getETag(cacheKey))) { 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) .header("Location", appStoreLink)
.build(); .build();
} else { } else {
return ResponseEntity.status(HttpStatus.NOT_FOUND) return response(HttpStatus.NOT_FOUND);
.build();
} }
} }

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

@ -164,6 +164,7 @@ public class SystemInfoController extends BaseController {
} else { } else {
infoObject.put("version", "unknown"); infoObject.put("version", "unknown");
} }
infoObject.put("type", "CE");
return infoObject; 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 io.swagger.v3.oas.annotations.media.Schema;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus; 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.rule.engine.api.MailService;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User; 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.UserEmailInfo;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; 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 USER_ID = "userId";
public static final String PATHS = "paths"; 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 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"; public static final String MOBILE_TOKEN_HEADER = "X-Mobile-Token";
@Value("${security.user_token_access_enabled}") @Value("${security.user_token_access_enabled}")
@ -130,12 +129,8 @@ public class UserController extends BaseController {
private final SystemSecurityService systemSecurityService; private final SystemSecurityService systemSecurityService;
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
private final TbUserService tbUserService; private final TbUserService tbUserService;
private final EntityQueryService entityQueryService;
@Autowired private final EntityService entityService;
private EntityQueryService entityQueryService;
@Autowired
private EntityService entityService;
@ApiOperation(value = "Get User (getUserById)", @ApiOperation(value = "Get User (getUserById)",
notes = "Fetch the User object based on the provided User Id. " + notes = "Fetch the User object based on the provided User Id. " +
@ -212,7 +207,7 @@ public class UserController extends BaseController {
public User saveUser( public User saveUser(
@Parameter(description = "A JSON value representing the User.", required = true) @Parameter(description = "A JSON value representing the User.", required = true)
@RequestBody User user, @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 { @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, HttpServletRequest request) throws ThingsboardException {
if (!Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { if (!Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
user.setTenantId(getCurrentUser().getTenantId()); user.setTenantId(getCurrentUser().getTenantId());
@ -230,45 +225,39 @@ public class UserController extends BaseController {
@Parameter(description = "Email of the user", required = true) @Parameter(description = "Email of the user", required = true)
@RequestParam(value = "email") String email, @RequestParam(value = "email") String email,
HttpServletRequest request) throws ThingsboardException { HttpServletRequest request) throws ThingsboardException {
User user = checkNotNull(userService.findUserByEmail(getCurrentUser().getTenantId(), email)); SecurityUser securityUser = getCurrentUser();
User user = checkNotNull(userService.findUserByEmail(securityUser.getTenantId(), email));
accessControlService.checkPermission(getCurrentUser(), Resource.USER, Operation.READ, accessControlService.checkPermission(securityUser, Resource.USER, Operation.READ, user.getId(), user);
user.getId(), user);
UserCredentials userCredentials = userService.findUserCredentialsByUserId(getCurrentUser().getTenantId(), user.getId()); UserActivationLink activationLink = tbUserService.getActivationLink(securityUser.getTenantId(), securityUser.getCustomerId(), user.getId(), request);
if (!userCredentials.isEnabled() && userCredentials.getActivateToken() != null) { mailService.sendActivationEmail(activationLink.value(), activationLink.ttlMs(), email);
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);
}
} }
@ApiOperation(value = "Get the activation link (getActivationLink)", @ApiOperation(value = "Get activation link (getActivationLink)",
notes = "Get the activation link for the user. " + 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) "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')") @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 @ResponseBody
public String getActivationLink( public String getActivationLink(@Parameter(description = USER_ID_PARAM_DESCRIPTION)
@Parameter(description = USER_ID_PARAM_DESCRIPTION) @PathVariable(USER_ID) String strUserId,
@PathVariable(USER_ID) String strUserId, HttpServletRequest request) throws ThingsboardException {
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); checkParameter(USER_ID, strUserId);
UserId userId = new UserId(toUUID(strUserId)); UserId userId = new UserId(toUUID(strUserId));
User user = checkUserId(userId, Operation.READ); checkUserId(userId, Operation.READ);
SecurityUser authUser = getCurrentUser(); SecurityUser securityUser = getCurrentUser();
UserCredentials userCredentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), user.getId()); return tbUserService.getActivationLink(securityUser.getTenantId(), securityUser.getCustomerId(), userId, request);
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);
}
} }
@ApiOperation(value = "Delete User (deleteUser)", @ApiOperation(value = "Delete User (deleteUser)",
@ -411,7 +400,7 @@ public class UserController extends BaseController {
public void setUserCredentialsEnabled( public void setUserCredentialsEnabled(
@Parameter(description = USER_ID_PARAM_DESCRIPTION) @Parameter(description = USER_ID_PARAM_DESCRIPTION)
@PathVariable(USER_ID) String strUserId, @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 { @RequestParam(required = false, defaultValue = "true") boolean userCredentialsEnabled) throws ThingsboardException {
checkParameter(USER_ID, strUserId); checkParameter(USER_ID, strUserId);
UserId userId = new UserId(toUUID(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.alarm.EntityAlarm;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.domain.Domain; 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.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType; 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.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
@ -134,6 +136,17 @@ public class EdgeEventSourcingListener {
return; return;
} }
try { 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); log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event);
tbClusterService.sendNotificationMsgToEdge(event.getTenantId(), event.getEdgeId(), event.getEntityId(), tbClusterService.sendNotificationMsgToEdge(event.getTenantId(), event.getEdgeId(), event.getEntityId(),
event.getBody(), null, EdgeUtils.getEdgeEventActionTypeByActionType(event.getActionType()), 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) @TransactionalEventListener(fallbackExecution = true)
public void handleEvent(ActionEntityEvent<?> event) { public void handleEvent(ActionEntityEvent<?> event) {
executorService.submit(() -> { executorService.submit(() -> {
log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event);
try { try {
switch (event.getActionType()) { switch (event.getActionType()) {
case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE ->
@ -69,6 +70,7 @@ public class RelatedEdgesSourcingListener {
@TransactionalEventListener(fallbackExecution = true) @TransactionalEventListener(fallbackExecution = true)
public void handleEvent(DeleteEntityEvent<?> event) { public void handleEvent(DeleteEntityEvent<?> event) {
executorService.submit(() -> { executorService.submit(() -> {
log.trace("[{}] DeleteEntityEvent called: {}", event.getTenantId(), event);
try { try {
relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId()); relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId());
} catch (Exception e) { } 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.rule.engine.api.MailService;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User; 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.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; 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.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.security.system.SystemSecurityService;
import static org.thingsboard.server.controller.UserController.ACTIVATE_URL_PATTERN; import java.util.concurrent.TimeUnit;
@Service @Service
@TbCoreComponent @TbCoreComponent
@ -53,13 +55,9 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse
boolean sendEmail = tbUser.getId() == null && sendActivationMail; boolean sendEmail = tbUser.getId() == null && sendActivationMail;
User savedUser = checkNotNull(userService.saveUser(tenantId, tbUser)); User savedUser = checkNotNull(userService.saveUser(tenantId, tbUser));
if (sendEmail) { if (sendEmail) {
UserCredentials userCredentials = userService.findUserCredentialsByUserId(tenantId, savedUser.getId()); UserActivationLink activationLink = getActivationLink(tenantId, customerId, savedUser.getId(), request);
String baseUrl = systemSecurityService.getBaseUrl(tenantId, customerId, request);
String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
userCredentials.getActivateToken());
String email = savedUser.getEmail();
try { try {
mailService.sendActivationEmail(activateUrl, email); mailService.sendActivationEmail(activationLink.value(), activationLink.ttlMs(), savedUser.getEmail());
} catch (ThingsboardException e) { } catch (ThingsboardException e) {
userService.deleteUser(tenantId, savedUser); userService.deleteUser(tenantId, savedUser);
throw e; throw e;
@ -87,4 +85,24 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse
throw e; 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; package org.thingsboard.server.service.entitiy.user;
import jakarta.servlet.http.HttpServletRequest; 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.User;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
public interface TbUserService { public interface TbUserService {
User save(TenantId tenantId, CustomerId customerId, User tbUser, boolean sendActivationMail, HttpServletRequest request, User user) throws ThingsboardException; 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; 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); 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 @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); String subject = messages.getMessage("activation.subject", null, Locale.US);
Map<String, Object> model = new HashMap<>(); Map<String, Object> model = new HashMap<>();
model.put("activationLink", activationLink); model.put("activationLink", activationLink);
model.put("activationLinkTtlInHours", (int) Math.ceil(ttlMs / 3600000.0));
model.put(TARGET_EMAIL, email); model.put(TARGET_EMAIL, email);
String message = mergeTemplateIntoString("activation.ftl", model); String message = mergeTemplateIntoString("activation.ftl", model);
@ -188,12 +188,13 @@ public class DefaultMailService implements MailService {
} }
@Override @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); String subject = messages.getMessage("reset.password.subject", null, Locale.US);
Map<String, Object> model = new HashMap<>(); Map<String, Object> model = new HashMap<>();
model.put("passwordResetLink", passwordResetLink); model.put("passwordResetLink", passwordResetLink);
model.put("passwordResetLinkTtlInHours", (int) Math.ceil(ttlMs / 3600000.0));
model.put(TARGET_EMAIL, email); model.put(TARGET_EMAIL, email);
String message = mergeTemplateIntoString("reset.password.ftl", model); String message = mergeTemplateIntoString("reset.password.ftl", model);
@ -202,10 +203,10 @@ public class DefaultMailService implements MailService {
} }
@Override @Override
public void sendResetPasswordEmailAsync(String passwordResetLink, String email) { public void sendResetPasswordEmailAsync(String passwordResetLink, long ttlMs, String email) {
passwordResetExecutorService.execute(() -> { passwordResetExecutorService.execute(() -> {
try { try {
this.sendResetPasswordEmail(passwordResetLink, email); this.sendResetPasswordEmail(passwordResetLink, ttlMs, email);
} catch (Exception e) { } catch (Exception e) {
log.error("Error occurred: {} ", e.getMessage()); 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 @PostConstruct
private void postConstruct() throws IOException { 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 @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.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.dao.entity.AbstractCachedService; 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.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; 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 @Service
@Slf4j @Slf4j
@ -37,12 +38,12 @@ import static org.thingsboard.server.service.security.system.DefaultSystemSecuri
public class MobileAppSecretServiceImpl extends AbstractCachedService<String, JwtPair, MobileSecretEvictEvent> implements MobileAppSecretService { public class MobileAppSecretServiceImpl extends AbstractCachedService<String, JwtPair, MobileSecretEvictEvent> implements MobileAppSecretService {
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
private final SystemSecurityService systemSecurityService; private final SecuritySettingsService securitySettingsService;
@Override @Override
public String generateMobileAppSecret(SecurityUser securityUser) { public String generateMobileAppSecret(SecurityUser securityUser) {
log.trace("Executing generateSecret for user [{}]", securityUser.getId()); 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); String secret = StringUtils.generateSafeToken(mobileSecretKeyLength == null ? DEFAULT_MOBILE_SECRET_KEY_LENGTH : mobileSecretKeyLength);
cache.put(secret, tokenFactory.createTokenPair(securityUser)); cache.put(secret, tokenFactory.createTokenPair(securityUser));
return secret; return secret;
@ -63,4 +64,5 @@ public class MobileAppSecretServiceImpl extends AbstractCachedService<String, Jw
public void handleEvictEvent(MobileSecretEvictEvent event) { public void handleEvictEvent(MobileSecretEvictEvent event) {
cache.evict(event.getSecret()); 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.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.exception.DataValidationException; 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.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken; import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
@ -58,6 +59,7 @@ import java.util.UUID;
public class RestAuthenticationProvider implements AuthenticationProvider { public class RestAuthenticationProvider implements AuthenticationProvider {
private final SystemSecurityService systemSecurityService; private final SystemSecurityService systemSecurityService;
private final SecuritySettingsService securitySettingsService;
private final UserService userService; private final UserService userService;
private final CustomerService customerService; private final CustomerService customerService;
private final TwoFactorAuthService twoFactorAuthService; private final TwoFactorAuthService twoFactorAuthService;
@ -66,10 +68,12 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
public RestAuthenticationProvider(final UserService userService, public RestAuthenticationProvider(final UserService userService,
final CustomerService customerService, final CustomerService customerService,
final SystemSecurityService systemSecurityService, final SystemSecurityService systemSecurityService,
SecuritySettingsService securitySettingsService,
TwoFactorAuthService twoFactorAuthService) { TwoFactorAuthService twoFactorAuthService) {
this.userService = userService; this.userService = userService;
this.customerService = customerService; this.customerService = customerService;
this.systemSecurityService = systemSecurityService; this.systemSecurityService = systemSecurityService;
this.securitySettingsService = securitySettingsService;
this.twoFactorAuthService = twoFactorAuthService; this.twoFactorAuthService = twoFactorAuthService;
} }
@ -82,13 +86,13 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
throw new BadCredentialsException("Authentication Failed. Bad user principal."); throw new BadCredentialsException("Authentication Failed. Bad user principal.");
} }
UserPrincipal userPrincipal = (UserPrincipal) principal; UserPrincipal userPrincipal = (UserPrincipal) principal;
SecurityUser securityUser; SecurityUser securityUser;
if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) { if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) {
String username = userPrincipal.getValue(); String username = userPrincipal.getValue();
String password = (String) authentication.getCredentials(); String password = (String) authentication.getCredentials();
SecuritySettings securitySettings = systemSecurityService.getSecuritySettings(); SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy(); UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy();
if (Boolean.TRUE.equals(passwordPolicy.getForceUserToResetPasswordIfNotValid())) { if (Boolean.TRUE.equals(passwordPolicy.getForceUserToResetPasswordIfNotValid())) {
try { 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.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.passay.CharacterRule; import org.passay.CharacterRule;
import org.passay.EnglishCharacterData; import org.passay.EnglishCharacterData;
@ -29,9 +29,6 @@ import org.passay.PasswordValidator;
import org.passay.Rule; import org.passay.Rule;
import org.passay.RuleResult; import org.passay.RuleResult;
import org.passay.WhitespaceRule; 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.BadCredentialsException;
import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException; 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.audit.AuditLogService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.settings.AdminSettingsService; 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.UserService;
import org.thingsboard.server.dao.user.UserServiceImpl; import org.thingsboard.server.dao.user.UserServiceImpl;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
@ -68,76 +66,23 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE;
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor
public class DefaultSystemSecurityService implements SystemSecurityService { public class DefaultSystemSecurityService implements SystemSecurityService {
public static final int DEFAULT_MOBILE_SECRET_KEY_LENGTH = 64; private final AdminSettingsService adminSettingsService;
private final BCryptPasswordEncoder encoder;
@Autowired private final UserService userService;
private AdminSettingsService adminSettingsService; private final MailService mailService;
private final AuditLogService auditLogService;
@Autowired private final SecuritySettingsService securitySettingsService;
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);
}
}
@Override @Override
public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException { public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException {
if (!encoder.matches(password, userCredentials.getPassword())) { if (!encoder.matches(password, userCredentials.getPassword())) {
int failedLoginAttempts = userService.increaseFailedLoginAttempts(tenantId, userCredentials.getUserId()); int failedLoginAttempts = userService.increaseFailedLoginAttempts(tenantId, userCredentials.getUserId());
SecuritySettings securitySettings = self.getSecuritySettings(); SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) { if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) {
if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) { if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) {
lockAccount(userCredentials.getUserId(), username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts()); lockAccount(userCredentials.getUserId(), username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts());
@ -153,7 +98,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
userService.resetFailedLoginAttempts(tenantId, userCredentials.getUserId()); userService.resetFailedLoginAttempts(tenantId, userCredentials.getUserId());
SecuritySettings securitySettings = self.getSecuritySettings(); SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) { if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) {
if ((userCredentials.getCreatedTime() if ((userCredentials.getCreatedTime()
+ TimeUnit.DAYS.toMillis(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) + TimeUnit.DAYS.toMillis(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays()))
@ -181,7 +126,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
if (maxVerificationFailures != null && maxVerificationFailures > 0 if (maxVerificationFailures != null && maxVerificationFailures > 0
&& failedVerificationAttempts >= maxVerificationFailures) { && failedVerificationAttempts >= maxVerificationFailures) {
userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false); userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false);
SecuritySettings securitySettings = self.getSecuritySettings(); SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
lockAccount(userId, securityUser.getEmail(), securitySettings.getUserLockoutNotificationEmail(), maxVerificationFailures); lockAccount(userId, securityUser.getEmail(), securitySettings.getUserLockoutNotificationEmail(), maxVerificationFailures);
throw new LockedException("User account was locked due to exceeded 2FA verification attempts"); throw new LockedException("User account was locked due to exceeded 2FA verification attempts");
} }
@ -200,7 +145,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
@Override @Override
public void validatePassword(String password, UserCredentials userCredentials) throws DataValidationException { public void validatePassword(String password, UserCredentials userCredentials) throws DataValidationException {
SecuritySettings securitySettings = self.getSecuritySettings(); SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy(); UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy();
validatePasswordByPolicy(password, passwordPolicy); validatePasswordByPolicy(password, passwordPolicy);
@ -330,4 +275,5 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
private static boolean isPositiveInteger(Integer val) { private static boolean isPositiveInteger(Integer val) {
return val != null && val.intValue() > 0; 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.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials; 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.UserPasswordPolicy;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
@ -30,10 +29,6 @@ import org.thingsboard.server.service.security.model.SecurityUser;
public interface SystemSecurityService { public interface SystemSecurityService {
SecuritySettings getSecuritySettings();
SecuritySettings saveSecuritySettings(SecuritySettings securitySettings);
void validatePasswordByPolicy(String password, UserPasswordPolicy passwordPolicy); void validatePasswordByPolicy(String password, UserPasswordPolicy passwordPolicy);
void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException; 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, Exception e);
void logLoginAction(User user, Object authenticationDetails, ActionType actionType, String provider, 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 { public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubscriptionService {
private static final int DEFAULT_LIMIT = 100; 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 @Autowired
@Lazy @Lazy
@ -495,7 +495,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
} }
private TbEntityDataSubCtx createSubCtx(WebSocketSessionRef sessionRef, EntityDataCmd cmd) { 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, TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription); attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription);
if (cmd.getQuery() != null) { if (cmd.getQuery() != null) {
@ -506,7 +506,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
} }
private TbEntityCountSubCtx createSubCtx(WebSocketSessionRef sessionRef, EntityCountCmd cmd) { 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, TbEntityCountSubCtx ctx = new TbEntityCountSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, sessionRef, cmd.getCmdId()); attributesService, stats, sessionRef, cmd.getCmdId());
if (cmd.getQuery() != null) { if (cmd.getQuery() != null) {
@ -518,7 +518,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
private TbAlarmDataSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmDataCmd cmd) { 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, TbAlarmDataSubCtx ctx = new TbAlarmDataSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription, attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription,
maxAlarmQueriesPerRefreshInterval); maxAlarmQueriesPerRefreshInterval);
@ -528,7 +528,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
} }
private TbAlarmCountSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) { 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, TbAlarmCountSubCtx ctx = new TbAlarmCountSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, alarmService, sessionRef, cmd.getCmdId()); attributesService, stats, alarmService, sessionRef, cmd.getCmdId());
if (cmd.getQuery() != null) { 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 @Service
public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionService { 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, TbEntityLocalSubsInfo> subscriptionsByEntityId = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, TbEntityUpdatesInfo> entityUpdates = 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>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"> <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"> <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> </td>
</tr> </tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"> <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>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"> <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"> <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> </td>
</tr> </tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"> <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">

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

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

@ -16,20 +16,30 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.assertj.core.data.Offset;
import org.junit.After; import org.junit.After;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil; 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.Authority;
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.SecuritySettings;
import org.thingsboard.server.dao.service.DaoSqlTest; 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.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.model.ChangePasswordRequest; import org.thingsboard.server.service.security.model.ChangePasswordRequest;
import java.util.concurrent.TimeUnit; 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.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
@ -39,55 +49,55 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@DaoSqlTest @DaoSqlTest
public class AuthControllerTest extends AbstractControllerTest { public class AuthControllerTest extends AbstractControllerTest {
@SpyBean
private UserCredentialsDao userCredentialsDao;
@After @After
public void tearDown() throws Exception { public void tearDown() throws Exception {
loginSysAdmin(); loginSysAdmin();
SecuritySettings securitySettings = doGet("/api/admin/securitySettings", SecuritySettings.class); updateSecuritySettings(securitySettings -> {
securitySettings.getPasswordPolicy().setMaximumLength(72);
securitySettings.getPasswordPolicy().setMaximumLength(72); securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(false);
securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(false); });
doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk());
} }
@Test @Test
public void testGetUser() throws Exception { public void testGetUser() throws Exception {
doGet("/api/auth/user") doGet("/api/auth/user")
.andExpect(status().isUnauthorized()); .andExpect(status().isUnauthorized());
loginSysAdmin(); loginSysAdmin();
doGet("/api/auth/user") doGet("/api/auth/user")
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name()))) .andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL))); .andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
loginTenantAdmin(); loginTenantAdmin();
doGet("/api/auth/user") doGet("/api/auth/user")
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.authority",is(Authority.TENANT_ADMIN.name()))) .andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name())))
.andExpect(jsonPath("$.email",is(TENANT_ADMIN_EMAIL))); .andExpect(jsonPath("$.email", is(TENANT_ADMIN_EMAIL)));
loginCustomerUser(); loginCustomerUser();
doGet("/api/auth/user") doGet("/api/auth/user")
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.authority",is(Authority.CUSTOMER_USER.name()))) .andExpect(jsonPath("$.authority", is(Authority.CUSTOMER_USER.name())))
.andExpect(jsonPath("$.email",is(CUSTOMER_USER_EMAIL))); .andExpect(jsonPath("$.email", is(CUSTOMER_USER_EMAIL)));
} }
@Test @Test
public void testLoginLogout() throws Exception { public void testLoginLogout() throws Exception {
loginSysAdmin(); loginSysAdmin();
doGet("/api/auth/user") doGet("/api/auth/user")
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name()))) .andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL))); .andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
TimeUnit.SECONDS.sleep(1); //We need to make sure that event for invalidating token was successfully processed TimeUnit.SECONDS.sleep(1); //We need to make sure that event for invalidating token was successfully processed
logout(); logout();
doGet("/api/auth/user") doGet("/api/auth/user")
.andExpect(status().isUnauthorized()); .andExpect(status().isUnauthorized());
resetTokens(); resetTokens();
} }
@ -97,14 +107,14 @@ public class AuthControllerTest extends AbstractControllerTest {
loginSysAdmin(); loginSysAdmin();
doGet("/api/auth/user") doGet("/api/auth/user")
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name()))) .andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL))); .andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
refreshToken(); refreshToken();
doGet("/api/auth/user") doGet("/api/auth/user")
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name()))) .andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL))); .andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
} }
@Test @Test
@ -131,10 +141,10 @@ public class AuthControllerTest extends AbstractControllerTest {
loginUser(TENANT_ADMIN_EMAIL, newPassword); loginUser(TENANT_ADMIN_EMAIL, newPassword);
loginSysAdmin(); loginSysAdmin();
SecuritySettings securitySettings = doGet("/api/admin/securitySettings", SecuritySettings.class); updateSecuritySettings(securitySettings -> {
securitySettings.getPasswordPolicy().setMaximumLength(15); securitySettings.getPasswordPolicy().setMaximumLength(15);
securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(true); securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(true);
doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk()); });
//try to login with user password that is not valid after security settings was updated //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)) 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."))); .andExpect(jsonPath("$.message", is("The entered password violates our policies. If this is your real password, please reset it.")));
} }
@Test @Test
public void testShouldNotResetPasswordToTooLongValue() throws Exception { public void testShouldNotResetPasswordToTooLongValue() throws Exception {
loginTenantAdmin(); loginTenantAdmin();
@ -163,9 +174,95 @@ public class AuthControllerTest extends AbstractControllerTest {
Mockito.doNothing().when(mailService).sendPasswordWasResetEmail(anyString(), anyString()); Mockito.doNothing().when(mailService).sendPasswordWasResetEmail(anyString(), anyString());
doPost("/api/noauth/resetPassword", resetPasswordRequest) doPost("/api/noauth/resetPassword", resetPasswordRequest)
.andExpect(status().isBadRequest()) .andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message", .andExpect(jsonPath("$.message",
is("Password must be no more than 72 characters in length."))); 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 @Test
@ -173,4 +270,19 @@ public class AuthControllerTest extends AbstractControllerTest {
doGet("/login").andExpect(status().isOk()); doGet("/login").andExpect(status().isOk());
doGet("/home").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.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Random;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -124,6 +125,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
protected EdgeImitator edgeImitator; protected EdgeImitator edgeImitator;
protected Edge edge; protected Edge edge;
private Random random = new Random();
@Autowired @Autowired
protected EdgeEventService edgeEventService; protected EdgeEventService edgeEventService;
@ -163,15 +166,10 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
} }
private RuleChainId getEdgeRootRuleChainId() throws Exception { private RuleChainId getEdgeRootRuleChainId() throws Exception {
List<RuleChain> edgeRuleChains = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/ruleChains?", return doGetTypedWithPageLink("/api/ruleChains?type={type}&", new TypeReference<PageData<RuleChain>>() {},
new TypeReference<PageData<RuleChain>>() { new PageLink(100, 0, "Edge Root Rule Chain"),
}, new PageLink(100)).getData(); "EDGE")
for (RuleChain edgeRuleChain : edgeRuleChains) { .getData().get(0).getId();
if (edgeRuleChain.isRoot()) {
return edgeRuleChain.getId();
}
}
throw new RuntimeException("Root rule chain not found");
} }
@After @After
@ -196,6 +194,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
Asset savedAsset = saveAsset("Edge Asset 1"); Asset savedAsset = saveAsset("Edge Asset 1");
updateRootRuleChainMetadata();
edge = doPost("/api/edge", constructEdge("Test Edge", "test"), Edge.class); edge = doPost("/api/edge", constructEdge("Test Edge", "test"), Edge.class);
doPost("/api/edge/" + edge.getUuidId() doPost("/api/edge/" + edge.getUuidId()
@ -207,6 +207,13 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
TimeUnit.MILLISECONDS.sleep(1000); 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) { protected void extendDeviceProfileData(DeviceProfile deviceProfile) {
DeviceProfileData profileData = deviceProfile.getProfileData(); DeviceProfileData profileData = deviceProfile.getProfileData();
List<DeviceProfileAlarm> alarms = new ArrayList<>(); 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.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; 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.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg; 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 { public class DashboardEdgeTest extends AbstractEdgeTest {
private static final int MOBILE_ORDER = 5; 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 @Test
public void testDashboards() throws Exception { public void testDashboards() throws Exception {
// create dashboard and assign to edge // create dashboard and assign to edge
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(2);
Dashboard dashboard = new Dashboard(); Dashboard dashboard = new Dashboard();
dashboard.setTitle("Edge Test Dashboard"); dashboard.setTitle("Edge Test Dashboard");
dashboard.setMobileHide(true); dashboard.setMobileHide(true);
@ -59,23 +60,26 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
doPost("/api/edge/" + edge.getUuidId() doPost("/api/edge/" + edge.getUuidId()
+ "/dashboard/" + savedDashboard.getUuidId(), Dashboard.class); + "/dashboard/" + savedDashboard.getUuidId(), Dashboard.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Optional<DashboardUpdateMsg> dashboardUpdateMsgOpt = edgeImitator.findMessageByType(DashboardUpdateMsg.class);
Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); Assert.assertTrue(dashboardUpdateMsgOpt.isPresent());
DashboardUpdateMsg dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; DashboardUpdateMsg dashboardUpdateMsg = dashboardUpdateMsgOpt.get();
Dashboard dashboardMsg = JacksonUtil.fromString(dashboardUpdateMsg.getEntity(), Dashboard.class, true); Dashboard dashboardMsg = JacksonUtil.fromString(dashboardUpdateMsg.getEntity(), Dashboard.class, true);
Assert.assertNotNull(dashboardMsg); Assert.assertNotNull(dashboardMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType());
Assert.assertEquals(savedDashboard, dashboardMsg); 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()); Assert.assertEquals(MOBILE_ORDER, dashboardMsg.getMobileOrder().intValue());
testAutoGeneratedCodeByProtobuf(dashboardUpdateMsg); testAutoGeneratedCodeByProtobuf(dashboardUpdateMsg);
Optional<ResourceUpdateMsg> resourceUpdateMsg = edgeImitator.findMessageByType(ResourceUpdateMsg.class);
Assert.assertTrue(resourceUpdateMsg.isPresent());
// update dashboard // update dashboard
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
savedDashboard.setTitle("Updated Edge Test Dashboard"); savedDashboard.setTitle("Updated Edge Test Dashboard");
savedDashboard = doPost("/api/dashboard", savedDashboard, Dashboard.class); savedDashboard = doPost("/api/dashboard", savedDashboard, Dashboard.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage(); AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg);
dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage;
dashboardMsg = JacksonUtil.fromString(dashboardUpdateMsg.getEntity(), Dashboard.class, true); 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); 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 @Test
public void testSetRootRuleChain() throws Exception { public void testSetRootRuleChain() throws Exception {
// create rule chain // 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)); return SimpleTbCacheValueWrapper.wrap(cache.get(key));
} }
@Override
public TbCacheValueWrapper<V> get(K key, boolean transactionMode) {
return get(key);
}
@Override @Override
public void put(K key, V value) { public void put(K key, V value) {
lock.lock(); 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 @Override
public void put(K key, V value) { public void put(K key, V value) {
cache.put(key, value, connection, true); cache.put(key, value, connection);
} }
@Override @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 @Override
public TbCacheValueWrapper<V> get(K key) { public TbCacheValueWrapper<V> get(K key) {
return get(key, false);
}
@Override
public TbCacheValueWrapper<V> get(K key, boolean transactionMode) {
if (!cacheEnabled) { if (!cacheEnabled) {
return null; return null;
} }
try (var connection = connectionFactory.getConnection()) { try (var connection = connectionFactory.getConnection()) {
byte[] rawKey = getRawKey(key); byte[] rawValue = doGet(key, connection);
byte[] rawValue = doGet(connection, rawKey, transactionMode);
if (rawValue == null || rawValue.length == 0) { if (rawValue == null || rawValue.length == 0) {
return null; return null;
} else if (Arrays.equals(rawValue, BINARY_NULL_VALUE)) { } 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) { protected byte[] doGet(K key, RedisConnection connection) {
return connection.stringCommands().get(rawKey); return connection.stringCommands().get(getRawKey(key));
} }
@Override @Override
@ -124,11 +118,11 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
return; return;
} }
try (var connection = connectionFactory.getConnection()) { 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); 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);
TbCacheValueWrapper<V> get(K key, boolean transactionMode);
void put(K key, V value); void put(K key, V value);
void putIfAbsent(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) { if (putToCache) {
return getAndPutInTransaction(key, dbCall, cacheNullValue); return getAndPutInTransaction(key, dbCall, cacheNullValue);
} else { } else {
TbCacheValueWrapper<V> cacheValueWrapper = get(key, true); TbCacheValueWrapper<V> cacheValueWrapper = get(key);
if (cacheValueWrapper != null) { if (cacheValueWrapper != null) {
return cacheValueWrapper.get(); 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) { 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) { if (cacheValueWrapper != null) {
V cacheValue = cacheValueWrapper.get(); V cacheValue = cacheValueWrapper.get();
return cacheValue != null ? cacheValueToResult.apply(cacheValue) : null; return cacheValue != null ? cacheValueToResult.apply(cacheValue) : null;
@ -92,7 +90,7 @@ public interface TbTransactionalCache<K extends Serializable, V extends Serializ
if (putToCache) { if (putToCache) {
return getAndPutInTransaction(key, dbCall, cacheValueToResult, dbValueToCacheValue, cacheNullValue); return getAndPutInTransaction(key, dbCall, cacheValueToResult, dbValueToCacheValue, cacheNullValue);
} else { } else {
TbCacheValueWrapper<V> cacheValueWrapper = get(key, true); TbCacheValueWrapper<V> cacheValueWrapper = get(key);
if (cacheValueWrapper != null) { if (cacheValueWrapper != null) {
var cacheValue = cacheValueWrapper.get(); var cacheValue = cacheValueWrapper.get();
return cacheValue == null ? null : cacheValueToResult.apply(cacheValue); 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; 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) { public VersionedCaffeineTbCache(CacheManager cacheManager, String cacheName) {
super(cacheManager, 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; import java.util.Arrays;
@Slf4j @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 VERSION_SIZE = 8;
private static final int VALUE_END_OFFSET = -1; private static final int VALUE_END_OFFSET = -1;
@ -79,15 +79,20 @@ public abstract class VersionedRedisTbCache<K extends Serializable, V extends Se
} }
@Override @Override
protected byte[] doGet(RedisConnection connection, byte[] rawKey, boolean transactionMode) { protected byte[] doGet(K key, RedisConnection connection) {
if (transactionMode) { if (!key.isVersioned()) {
return super.doGet(connection, rawKey, true); return super.doGet(key, connection);
} }
byte[] rawKey = getRawKey(key);
return connection.stringCommands().getRange(rawKey, VERSION_SIZE, VALUE_END_OFFSET); return connection.stringCommands().getRange(rawKey, VERSION_SIZE, VALUE_END_OFFSET);
} }
@Override @Override
public void put(K key, V value) { public void put(K key, V value) {
if (!key.isVersioned()) {
super.put(key, value);
return;
}
Long version = getVersion(value); Long version = getVersion(value);
if (version == null) { if (version == null) {
return; return;
@ -96,9 +101,9 @@ public abstract class VersionedRedisTbCache<K extends Serializable, V extends Se
} }
@Override @Override
public void put(K key, V value, RedisConnection connection, boolean transactionMode) { public void put(K key, V value, RedisConnection connection) {
if (transactionMode) { if (!key.isVersioned()) {
super.put(key, value, connection, true); // because scripting commands are not supported in transaction mode super.put(key, value, connection); // because scripting commands are not supported in transaction mode
return; return;
} }
Long version = getVersion(value); 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.Optional;
import java.util.function.Supplier; 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); 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.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.thingsboard.server.cache.VersionedCacheKey;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable;
@Getter @Getter
@EqualsAndHashCode @EqualsAndHashCode
@RequiredArgsConstructor @RequiredArgsConstructor
@Builder @Builder
public class DeviceCacheKey implements Serializable { public class DeviceCacheKey implements VersionedCacheKey {
@Serial @Serial
private static final long serialVersionUID = 6366389552842340207L; 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 requestExpiredPasswordReset(TenantId tenantId, UserCredentialsId userCredentialsId);
UserCredentials generatePasswordResetToken(UserCredentials userCredentials);
UserCredentials generateUserActivationToken(UserCredentials userCredentials);
UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials); UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials);
void deleteUser(TenantId tenantId, User user); 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; package org.thingsboard.server.common.data.security;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JsonNode; import lombok.Data;
import lombok.EqualsAndHashCode; 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.UserCredentialsId;
import org.thingsboard.server.common.data.id.UserId; 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 java.io.Serial;
import static org.thingsboard.server.common.data.BaseDataWithAdditionalInfo.setJson;
@Data
@EqualsAndHashCode(callSuper = true) @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 static final long serialVersionUID = -2108436378880529163L;
private UserId userId; private UserId userId;
private boolean enabled; private boolean enabled;
private String password; private String password;
private String activateToken; private String activateToken;
private Long activateTokenExpTime;
private String resetToken; 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() { public UserCredentials() {
super(); super();
} }
@ -59,75 +49,25 @@ public class UserCredentials extends BaseData<UserCredentialsId> {
super(id); 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() { @JsonIgnore
return activateToken; public boolean isActivationTokenExpired() {
return getActivationTokenTtl() == 0;
} }
public void setActivateToken(String activateToken) { @JsonIgnore
this.activateToken = activateToken; public long getActivationTokenTtl() {
} return activateTokenExpTime != null ? Math.max(activateTokenExpTime - System.currentTimeMillis(), 0) : 0;
public String getResetToken() {
return resetToken;
} }
public void setResetToken(String resetToken) { @JsonIgnore
this.resetToken = resetToken; public boolean isResetTokenExpired() {
return getResetTokenTtl() == 0;
} }
@Override @JsonIgnore
public String toString() { public long getResetTokenTtl() {
StringBuilder builder = new StringBuilder(); return resetTokenExpTime != null ? Math.max(resetTokenExpTime - System.currentTimeMillis(), 0) : 0;
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();
} }
} }

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; package org.thingsboard.server.common.data.security.model;
import io.swagger.v3.oas.annotations.media.Schema; 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 lombok.Data;
import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
@Schema @Schema
@Data @Data
public class SecuritySettings implements Serializable { public class SecuritySettings implements Serializable {
@Serial
private static final long serialVersionUID = -1307613974597312465L; private static final long serialVersionUID = -1307613974597312465L;
@Schema(description = "The user password policy object." ) @Schema(description = "The user password policy object.")
private UserPasswordPolicy passwordPolicy; 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; 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; private String userLockoutNotificationEmail;
@Schema(description = "Mobile secret key length" )
@Schema(description = "Mobile secret key length")
private Integer mobileSecretKeyLength; 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;
} }

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.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.Reader; import java.io.Reader;
import java.io.Writer; import java.io.Writer;
import java.nio.file.Files; 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() { public static ObjectNode newObjectNode() {
return newObjectNode(OBJECT_MAPPER); 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; package org.thingsboard.server.dao.asset;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.cache.VersionedCacheKey;
import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable;
@Data @Data
public class AssetProfileCacheKey implements Serializable { public class AssetProfileCacheKey implements VersionedCacheKey {
@Serial @Serial
private static final long serialVersionUID = 8220455917177676472L; private static final long serialVersionUID = 8220455917177676472L;
@ -63,4 +63,9 @@ public class AssetProfileCacheKey implements Serializable {
} }
} }
@Override
public boolean isVersioned() {
return assetProfileId != null;
}
} }

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.AllArgsConstructor;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.cache.VersionedCacheKey;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable;
@EqualsAndHashCode @EqualsAndHashCode
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public class AttributeCacheKey implements Serializable { public class AttributeCacheKey implements VersionedCacheKey {
@Serial @Serial
private static final long serialVersionUID = 2013369077925351881L; private static final long serialVersionUID = 2013369077925351881L;
@ -41,4 +41,9 @@ public class AttributeCacheKey implements Serializable {
return "{" + entityId + "}" + scope + "_" + key; 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; package org.thingsboard.server.dao.device;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.cache.VersionedCacheKey;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable;
@Data @Data
public class DeviceProfileCacheKey implements Serializable { public class DeviceProfileCacheKey implements VersionedCacheKey {
@Serial @Serial
private static final long serialVersionUID = 8220455917177676472L; private static final long serialVersionUID = 8220455917177676472L;
@ -74,4 +74,9 @@ public class DeviceProfileCacheKey implements Serializable {
return tenantId + "_" + name; return tenantId + "_" + name;
} }
@Override
public boolean isVersioned() {
return deviceProfileId != null;
}
} }

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

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.dao.edge; package org.thingsboard.server.dao.edge;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -30,6 +31,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
@Service @Service
@Slf4j
public class BaseRelatedEdgesService extends AbstractCachedEntityService<RelatedEdgesCacheKey, RelatedEdgesCacheValue, RelatedEdgesEvictEvent> implements RelatedEdgesService { public class BaseRelatedEdgesService extends AbstractCachedEntityService<RelatedEdgesCacheKey, RelatedEdgesCacheValue, RelatedEdgesEvictEvent> implements RelatedEdgesService {
public static final int RELATED_EDGES_CACHE_ITEMS = 1000; public static final int RELATED_EDGES_CACHE_ITEMS = 1000;
@ -47,6 +49,7 @@ public class BaseRelatedEdgesService extends AbstractCachedEntityService<Related
@Override @Override
public PageData<EdgeId> findEdgeIdsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) { 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)) { if (!pageLink.equals(FIRST_PAGE)) {
return edgeService.findEdgeIdsByTenantIdAndEntityId(tenantId, entityId, pageLink); return edgeService.findEdgeIdsByTenantIdAndEntityId(tenantId, entityId, pageLink);
} }
@ -56,6 +59,7 @@ public class BaseRelatedEdgesService extends AbstractCachedEntityService<Related
@Override @Override
public void publishRelatedEdgeIdsEvictEvent(TenantId tenantId, EntityId entityId) { public void publishRelatedEdgeIdsEvictEvent(TenantId tenantId, EntityId entityId) {
log.trace("Executing publishRelatedEdgeIdsEvictEvent, tenantId [{}], entityId [{}]", tenantId, entityId);
publishEvictEvent(new RelatedEdgesEvictEvent(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; package org.thingsboard.server.dao.entity;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.cache.VersionedCacheKey;
import org.thingsboard.server.cache.VersionedTbCache; import org.thingsboard.server.cache.VersionedTbCache;
import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.HasVersion;
import java.io.Serializable; 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 @Autowired
protected VersionedTbCache<K, V> cache; 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.Builder;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.cache.VersionedCacheKey;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable;
@Getter @Getter
@EqualsAndHashCode @EqualsAndHashCode
@Builder @Builder
public class EntityViewCacheKey implements Serializable { public class EntityViewCacheKey implements VersionedCacheKey {
@Serial @Serial
private static final long serialVersionUID = 5986277528222738163L; 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_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_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_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_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"; 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) @Column(name = ModelConstants.USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY, unique = true)
private String activateToken; 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) @Column(name = ModelConstants.USER_CREDENTIALS_RESET_TOKEN_PROPERTY, unique = true)
private String resetToken; private String resetToken;
@Column(name = ModelConstants.USER_CREDENTIALS_RESET_TOKEN_EXP_TIME_PROPERTY)
private Long resetTokenExpTime;
@Convert(converter = JsonConverter.class) @Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.USER_CREDENTIALS_ADDITIONAL_PROPERTY) @Column(name = ModelConstants.USER_CREDENTIALS_ADDITIONAL_PROPERTY)
private JsonNode additionalInfo; private JsonNode additionalInfo;
@ -72,7 +78,9 @@ public final class UserCredentialsEntity extends BaseSqlEntity<UserCredentials>
this.enabled = userCredentials.isEnabled(); this.enabled = userCredentials.isEnabled();
this.password = userCredentials.getPassword(); this.password = userCredentials.getPassword();
this.activateToken = userCredentials.getActivateToken(); this.activateToken = userCredentials.getActivateToken();
this.activateTokenExpTime = userCredentials.getActivateTokenExpTime();
this.resetToken = userCredentials.getResetToken(); this.resetToken = userCredentials.getResetToken();
this.resetTokenExpTime = userCredentials.getResetTokenExpTime();
this.additionalInfo = userCredentials.getAdditionalInfo(); this.additionalInfo = userCredentials.getAdditionalInfo();
} }
@ -86,7 +94,9 @@ public final class UserCredentialsEntity extends BaseSqlEntity<UserCredentials>
userCredentials.setEnabled(enabled); userCredentials.setEnabled(enabled);
userCredentials.setPassword(password); userCredentials.setPassword(password);
userCredentials.setActivateToken(activateToken); userCredentials.setActivateToken(activateToken);
userCredentials.setActivateTokenExpTime(activateTokenExpTime);
userCredentials.setResetToken(resetToken); userCredentials.setResetToken(resetToken);
userCredentials.setResetTokenExpTime(resetTokenExpTime);
userCredentials.setAdditionalInfo(additionalInfo); userCredentials.setAdditionalInfo(additionalInfo);
return userCredentials; 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); log.warn("[{}] Failed to create ruleChain relation. Edge Id: [{}]", ruleChainId, edgeId);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
if (!ruleChainId.equals(edge.getRootRuleChainId())) { eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).edgeId(edgeId).entityId(ruleChainId)
eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).edgeId(edgeId).entityId(ruleChainId) .actionType(ActionType.ASSIGNED_TO_EDGE).body(JacksonUtil.toString(edge)).build());
.actionType(ActionType.ASSIGNED_TO_EDGE).build());
}
return ruleChain; 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.AllArgsConstructor;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.cache.VersionedCacheKey;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable;
@EqualsAndHashCode @EqualsAndHashCode
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public class TsLatestCacheKey implements Serializable { public class TsLatestCacheKey implements VersionedCacheKey {
@Serial @Serial
private static final long serialVersionUID = 2024369077925351881L; private static final long serialVersionUID = 2024369077925351881L;
@ -39,4 +39,9 @@ public class TsLatestCacheKey implements Serializable {
return "{" + entityId + "}" + key; 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.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.dao.sql.JpaExecutorService; import org.thingsboard.server.dao.sql.JpaExecutorService;
import java.util.ArrayList; import java.util.ArrayList;
@ -72,6 +73,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.common.data.StringUtils.generateSafeToken; import static org.thingsboard.server.common.data.StringUtils.generateSafeToken;
import static org.thingsboard.server.dao.service.Validator.validateId; 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 UserAuthSettingsDao userAuthSettingsDao;
private final UserSettingsService userSettingsService; private final UserSettingsService userSettingsService;
private final UserSettingsDao userSettingsDao; private final UserSettingsDao userSettingsDao;
private final SecuritySettingsService securitySettingsService;
private final DataValidator<User> userValidator; private final DataValidator<User> userValidator;
private final DataValidator<UserCredentials> userCredentialsValidator; private final DataValidator<UserCredentials> userCredentialsValidator;
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
@ -178,9 +181,9 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
countService.publishCountEntityEvictEvent(savedUser.getTenantId(), EntityType.USER); countService.publishCountEntityEvictEvent(savedUser.getTenantId(), EntityType.USER);
UserCredentials userCredentials = new UserCredentials(); UserCredentials userCredentials = new UserCredentials();
userCredentials.setEnabled(false); userCredentials.setEnabled(false);
userCredentials.setActivateToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
userCredentials.setUserId(new UserId(savedUser.getUuidId())); userCredentials.setUserId(new UserId(savedUser.getUuidId()));
userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode()); userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode());
userCredentials = generateUserActivationToken(userCredentials);
userCredentialsDao.save(user.getTenantId(), userCredentials); userCredentialsDao.save(user.getTenantId(), userCredentials);
} }
eventPublisher.publishEvent(SaveEntityEvent.builder() eventPublisher.publishEvent(SaveEntityEvent.builder()
@ -242,8 +245,12 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
if (userCredentials.isEnabled()) { if (userCredentials.isEnabled()) {
throw new IncorrectParameterException("User credentials already activated"); throw new IncorrectParameterException("User credentials already activated");
} }
if (userCredentials.isActivationTokenExpired()) {
throw new IncorrectParameterException("Activation token expired");
}
userCredentials.setEnabled(true); userCredentials.setEnabled(true);
userCredentials.setActivateToken(null); userCredentials.setActivateToken(null);
userCredentials.setActivateTokenExpTime(null);
userCredentials.setPassword(password); userCredentials.setPassword(password);
if (userCredentials.getPassword() != null) { if (userCredentials.getPassword() != null) {
updatePasswordHistory(userCredentials); updatePasswordHistory(userCredentials);
@ -263,7 +270,7 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
if (!userCredentials.isEnabled()) { if (!userCredentials.isEnabled()) {
throw new DisabledException(String.format("User credentials not enabled [%s]", email)); throw new DisabledException(String.format("User credentials not enabled [%s]", email));
} }
userCredentials.setResetToken(generateSafeToken(DEFAULT_TOKEN_LENGTH)); userCredentials = generatePasswordResetToken(userCredentials);
return saveUserCredentials(tenantId, userCredentials); return saveUserCredentials(tenantId, userCredentials);
} }
@ -273,10 +280,26 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
if (!userCredentials.isEnabled()) { if (!userCredentials.isEnabled()) {
throw new IncorrectParameterException("Unable to reset password for inactive user"); throw new IncorrectParameterException("Unable to reset password for inactive user");
} }
userCredentials.setResetToken(generateSafeToken(DEFAULT_TOKEN_LENGTH)); userCredentials = generatePasswordResetToken(userCredentials);
return saveUserCredentials(tenantId, 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 @Override
public UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials) { public UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials) {
log.trace("Executing replaceUserCredentials [{}]", 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, id uuid NOT NULL CONSTRAINT user_credentials_pkey PRIMARY KEY,
created_time bigint NOT NULL, created_time bigint NOT NULL,
activate_token varchar(255) UNIQUE, activate_token varchar(255) UNIQUE,
activate_token_exp_time BIGINT,
enabled boolean, enabled boolean,
password varchar(255), password varchar(255),
reset_token varchar(255) UNIQUE, reset_token varchar(255) UNIQUE,
reset_token_exp_time BIGINT,
user_id uuid UNIQUE, user_id uuid UNIQUE,
additional_info varchar DEFAULT '{}' additional_info varchar DEFAULT '{}'
); );

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.setUserId(new UserId(UUID.randomUUID()));
userCredentials.setPassword("password"); userCredentials.setPassword("password");
userCredentials.setActivateToken("ACTIVATE_TOKEN_" + number); userCredentials.setActivateToken("ACTIVATE_TOKEN_" + number);
userCredentials.setActivateTokenExpTime(123L);
userCredentials.setResetToken("RESET_TOKEN_" + number); userCredentials.setResetToken("RESET_TOKEN_" + number);
userCredentials.setResetTokenExpTime(321L);
return userCredentialsDao.save(SYSTEM_TENANT_ID, userCredentials); return userCredentialsDao.save(SYSTEM_TENANT_ID, userCredentials);
} }

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 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 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; 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() this.webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient)) .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( .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(
(config.getMaxInMemoryBufferSizeInKb() > 0 ? config.getMaxInMemoryBufferSizeInKb() : 256) * 1024)) (config.getMaxInMemoryBufferSizeInKb() > 0 ? config.getMaxInMemoryBufferSizeInKb() : 256) * 1024))
.build(); .build();

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

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

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

@ -14,49 +14,17 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { import { Component, Injectable, Type, ɵComponentDef, ɵNG_COMP_DEF } from '@angular/core';
Compiler,
Component,
Injectable,
Injector,
NgModule,
NgModuleRef,
OnDestroy,
Type,
ɵresetCompiledComponents
} from '@angular/core';
import { from, Observable, of } from 'rxjs'; import { from, Observable, of } from 'rxjs';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { mergeMap } from 'rxjs/operators'; 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({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DynamicComponentFactoryService { export class DynamicComponentFactoryService {
private dynamicComponentModulesMap = new Map<Type<any>, DynamicComponentModuleData>(); constructor() {
constructor(private compiler: Compiler,
private injector: Injector) {
} }
public createDynamicComponent<T>( public createDynamicComponent<T>(
@ -64,66 +32,38 @@ export class DynamicComponentFactoryService {
template: string, template: string,
modules?: Type<any>[], modules?: Type<any>[],
preserveWhitespaces?: boolean, preserveWhitespaces?: boolean,
compileAttempt = 1, styles?: string[]): Observable<Type<T>> {
styles?: string[]): Observable<DynamicComponentData<T>> {
return from(import('@angular/compiler')).pipe( return from(import('@angular/compiler')).pipe(
mergeMap(() => { mergeMap(() => {
const comp = this._createDynamicComponent(componentType, template, preserveWhitespaces, styles); let componentImports: Type<any>[] = [CommonModule];
let moduleImports: Type<any>[] = [CommonModule];
if (modules) { if (modules) {
moduleImports = [...moduleImports, ...modules]; componentImports = [...componentImports, ...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;
}
} }
const comp = this.createAndCompileDynamicComponent(componentType, template, componentImports, preserveWhitespaces, styles);
return of(comp.type);
}) })
); );
} }
public destroyDynamicComponent<T>(componentType: Type<T>) { public destroyDynamicComponent<T>(_componentType: Type<T>) {
const moduleData = this.dynamicComponentModulesMap.get(componentType); }
if (moduleData) {
moduleData.moduleRef.destroy(); public getComponentDef<T>(componentType: Type<T>): ɵComponentDef<T> {
this.compiler.clearCacheFor(moduleData.moduleType); return componentType[ɵNG_COMP_DEF];
this.dynamicComponentModulesMap.delete(componentType);
}
} }
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 // noinspection AngularMissingOrInvalidDeclarationInModule
return Component({ const comp = Component({
template, template,
imports,
preserveWhitespaces, preserveWhitespaces,
styles styles,
standalone: true
})(componentType); })(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, Injectable,
Injector, Injector,
ModuleWithComponentFactories, ModuleWithComponentFactories,
Type Type, ɵNG_MOD_DEF
} from '@angular/core'; } from '@angular/core';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs'; import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs';
@ -50,7 +50,6 @@ export class ResourcesService {
private loadedJsonResources: { [url: string]: ReplaySubject<any> } = {}; private loadedJsonResources: { [url: string]: ReplaySubject<any> } = {};
private loadedResources: { [url: string]: ReplaySubject<void> } = {}; private loadedResources: { [url: string]: ReplaySubject<void> } = {};
private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {};
private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {}; private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {};
private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0]; 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>[] { private extractNgModules(module: any, modules: Type<any>[] = []): Type<any>[] {
try { try {
let potentialModules = [module]; let potentialModules = [module];
@ -274,7 +208,7 @@ export class ResourcesService {
while (potentialModules.length && currentScanDepth < 10) { while (potentialModules.length && currentScanDepth < 10) {
const newPotentialModules = []; const newPotentialModules = [];
for (const potentialModule of potentialModules) { for (const potentialModule of potentialModules) {
if (potentialModule && ('ɵmod' in potentialModule)) { if (potentialModule && (ɵNG_MOD_DEF in potentialModule)) {
modules.push(potentialModule); modules.push(potentialModule);
} else { } else {
for (const k of Object.keys(potentialModule)) { for (const k of Object.keys(potentialModule)) {
@ -349,7 +283,6 @@ export class ResourcesService {
} }
private clearModulesCache() { private clearModulesCache() {
this.loadedModules = {};
this.loadedModulesAndFactories = {}; this.loadedModulesAndFactories = {};
} }
} }

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 }}"> <input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </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> </div>
<tb-widget-actions-panel <tb-widget-actions-panel
formControlName="actions"> 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), []], cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []], borderRadius: [configData.config.borderRadius, []],
padding: [settings.padding, []],
actions: [configData.config.actions || {}, []] actions: [configData.config.actions || {}, []]
}); });
@ -183,6 +184,7 @@ export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
this.setCardButtons(config.cardButtons, this.widgetConfig.config); this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius; this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.settings.padding = config.padding;
this.widgetConfig.config.actions = config.actions; this.widgetConfig.config.actions = config.actions;
return this.widgetConfig; 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 }}"> <input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </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> </div>
<tb-widget-actions-panel <tb-widget-actions-panel
formControlName="actions"> 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), []], cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []], borderRadius: [configData.config.borderRadius, []],
padding: [settings.padding, []],
actions: [configData.config.actions || {}, []] actions: [configData.config.actions || {}, []]
}); });
@ -99,6 +100,7 @@ export class StatusWidgetBasicConfigComponent extends BasicWidgetConfigComponent
this.setCardButtons(config.cardButtons, this.widgetConfig.config); this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius; this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.settings.padding = config.padding;
this.widgetConfig.config.actions = config.actions; this.widgetConfig.config.actions = config.actions;
return this.widgetConfig; 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 formControlName="background">
</tb-background-settings> </tb-background-settings>
</div> </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 class="tb-form-row space-between column-lt-md">
<div translate>widget-config.show-card-buttons</div> <div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons"> <mat-chip-listbox multiple formControlName="cardButtons">

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

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

6
ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html

@ -208,6 +208,12 @@
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </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> </div>
<tb-widget-actions-panel <tb-widget-actions-panel
formControlName="actions"> formControlName="actions">

2
ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.ts

@ -147,6 +147,7 @@ export class WindSpeedDirectionBasicConfigComponent extends BasicWidgetConfigCom
cardButtons: [this.getCardButtons(configData.config), []], cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []], borderRadius: [configData.config.borderRadius, []],
padding: [settings.padding, []],
actions: [configData.config.actions || {}, []] actions: [configData.config.actions || {}, []]
}); });
@ -198,6 +199,7 @@ export class WindSpeedDirectionBasicConfigComponent extends BasicWidgetConfigCom
this.setCardButtons(config.cardButtons, this.widgetConfig.config); this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius; this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.settings.padding = config.padding;
this.widgetConfig.config.actions = config.actions; this.widgetConfig.config.actions = config.actions;
return this.widgetConfig; return this.widgetConfig;

13
ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts

@ -17,11 +17,12 @@
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { import {
Component, Component,
ComponentFactory, ComponentRef,
ComponentRef, HostBinding, HostBinding,
Inject, Inject,
Injector, NgModuleRef, Injector,
OnDestroy, Type, OnDestroy,
Type,
ViewContainerRef ViewContainerRef
} from '@angular/core'; } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
@ -35,13 +36,11 @@ import {
} from '@home/components/widget/dialog/custom-dialog.component'; } from '@home/components/widget/dialog/custom-dialog.component';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service';
export interface CustomDialogContainerData { export interface CustomDialogContainerData {
controller: (instance: CustomDialogComponent) => void; controller: (instance: CustomDialogComponent) => void;
data?: any; data?: any;
customComponentType: Type<CustomDialogComponent>; customComponentType: Type<CustomDialogComponent>;
customComponentModuleRef: NgModuleRef<DynamicComponentModule>;
} }
@Component({ @Component({
@ -80,7 +79,7 @@ export class CustomDialogContainerComponent extends DialogComponent<CustomDialog
}); });
try { try {
this.customComponentRef = this.viewContainerRef.createComponent(this.data.customComponentType, this.customComponentRef = this.viewContainerRef.createComponent(this.data.customComponentType,
{index: 0, injector, ngModuleRef: this.data.customComponentModuleRef}); {index: 0, injector});
} catch (e: any) { } catch (e: any) {
let message; let message;
if (e.message?.startsWith('NG0')) { if (e.message?.startsWith('NG0')) {

7
ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts

@ -60,11 +60,10 @@ export class CustomDialogService {
} }
return this.dynamicComponentFactoryService.createDynamicComponent( return this.dynamicComponentFactoryService.createDynamicComponent(
class CustomDialogComponentInstance extends CustomDialogComponent {}, template, modules).pipe( class CustomDialogComponentInstance extends CustomDialogComponent {}, template, modules).pipe(
mergeMap((componentData) => { mergeMap((componentType) => {
const dialogData: CustomDialogContainerData = { const dialogData: CustomDialogContainerData = {
controller, controller,
customComponentType: componentData.componentType, customComponentType: componentType,
customComponentModuleRef: componentData.componentModuleRef,
data data
}; };
let dialogConfig: MatDialogConfig = { let dialogConfig: MatDialogConfig = {
@ -79,7 +78,7 @@ export class CustomDialogService {
CustomDialogContainerComponent, CustomDialogContainerComponent,
dialogConfig).afterClosed().pipe( dialogConfig).afterClosed().pipe(
tap(() => { tap(() => {
this.dynamicComponentFactoryService.destroyDynamicComponent(componentData.componentType); this.dynamicComponentFactoryService.destroyDynamicComponent(componentType);
}) })
); );
} }

54
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html

@ -15,41 +15,41 @@
limitations under the License. limitations under the License.
--> -->
<div #valueCardPanel class="tb-value-card-panel" [class]="this.layout" [style]="backgroundStyle$ | async"> <div #valueCardPanel class="tb-value-card-panel" [class]="this.layout" [style.padding]="padding" [style]="backgroundStyle$ | async">
<div class="tb-value-card-overlay" [style]="overlayStyle"></div> <div class="tb-value-card-overlay" [style]="overlayStyle"></div>
<div class="tb-value-card-title-panel"> <div class="tb-value-card-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> <ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
</div> </div>
<div #valueCardContent class="tb-value-card-content" [class]="this.layout"> <div #valueCardContent class="tb-value-card-content" [class]="this.layout">
<ng-container [ngSwitch]="layout"> <ng-container [ngSwitch]="layout">
<ng-template [ngSwitchCase]="valueCardLayout.square"> <ng-template [ngSwitchCase]="valueCardLayout.square">
<ng-container *ngTemplateOutlet="iconWithLabelTpl"></ng-container> <ng-container *ngTemplateOutlet="iconWithLabelTpl"></ng-container>
<ng-container *ngTemplateOutlet="valueTpl"></ng-container> <ng-container *ngTemplateOutlet="valueTpl"></ng-container>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="valueCardLayout.vertical"> <ng-template [ngSwitchCase]="valueCardLayout.vertical">
<ng-container *ngTemplateOutlet="labelTpl"></ng-container> <ng-container *ngTemplateOutlet="labelTpl"></ng-container>
<ng-container *ngTemplateOutlet="valueTpl"></ng-container> <ng-container *ngTemplateOutlet="valueTpl"></ng-container>
<ng-container *ngTemplateOutlet="iconTpl"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="valueCardLayout.centered">
<ng-container *ngTemplateOutlet="labelTpl"></ng-container>
<div class="tb-value-card-icon-row">
<ng-container *ngTemplateOutlet="iconTpl"></ng-container> <ng-container *ngTemplateOutlet="iconTpl"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="valueCardLayout.centered">
<ng-container *ngTemplateOutlet="labelTpl"></ng-container>
<div class="tb-value-card-icon-row">
<ng-container *ngTemplateOutlet="iconTpl"></ng-container>
<ng-container *ngTemplateOutlet="valueTpl"></ng-container>
</div>
<ng-container *ngTemplateOutlet="dateTpl"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="valueCardLayout.simplified">
<ng-container *ngTemplateOutlet="valueTpl"></ng-container> <ng-container *ngTemplateOutlet="valueTpl"></ng-container>
</div> <ng-container *ngTemplateOutlet="labelTpl"></ng-container>
<ng-container *ngTemplateOutlet="dateTpl"></ng-container> </ng-template>
</ng-template> <ng-template [ngSwitchCase]="layout === valueCardLayout.horizontal ||
<ng-template [ngSwitchCase]="valueCardLayout.simplified"> layout === valueCardLayout.horizontal_reversed ? layout : ''">
<ng-container *ngTemplateOutlet="valueTpl"></ng-container> <ng-container *ngTemplateOutlet="iconWithLabelTpl"></ng-container>
<ng-container *ngTemplateOutlet="labelTpl"></ng-container> <div fxFlex></div>
</ng-template> <ng-container *ngTemplateOutlet="valueTpl"></ng-container>
<ng-template [ngSwitchCase]="layout === valueCardLayout.horizontal || </ng-template>
layout === valueCardLayout.horizontal_reversed ? layout : ''"> </ng-container>
<ng-container *ngTemplateOutlet="iconWithLabelTpl"></ng-container>
<div fxFlex></div>
<ng-container *ngTemplateOutlet="valueTpl"></ng-container>
</ng-template>
</ng-container>
</div> </div>
</div> </div>
<ng-template #iconWithLabelTpl> <ng-template #iconWithLabelTpl>

13
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts

@ -38,6 +38,7 @@ import {
getSingleTsValue, getSingleTsValue,
iconStyle, iconStyle,
overlayStyle, overlayStyle,
resolveCssSize,
textStyle textStyle
} from '@shared/models/widget-settings.models'; } from '@shared/models/widget-settings.models';
import { valueCardDefaultSettings, ValueCardLayout, ValueCardWidgetSettings } from './value-card-widget.models'; import { valueCardDefaultSettings, ValueCardLayout, ValueCardWidgetSettings } from './value-card-widget.models';
@ -48,7 +49,6 @@ import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
const squareLayoutSize = 160; const squareLayoutSize = 160;
const squareLayoutPadding = 48;
const horizontalLayoutHeight = 80; const horizontalLayoutHeight = 80;
@Component({ @Component({
@ -96,6 +96,7 @@ export class ValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestro
backgroundStyle$: Observable<ComponentStyle>; backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {}; overlayStyle: ComponentStyle = {};
padding: string;
private panelResize$: ResizeObserver; private panelResize$: ResizeObserver;
@ -148,6 +149,7 @@ export class ValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestro
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay); this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding;
} }
public ngAfterViewInit() { public ngAfterViewInit() {
@ -199,8 +201,13 @@ export class ValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestro
} }
private onResize() { private onResize() {
const panelWidth = this.valueCardPanel.nativeElement.getBoundingClientRect().width - squareLayoutPadding; const computedStyle = getComputedStyle(this.valueCardPanel.nativeElement);
const panelHeight = this.valueCardPanel.nativeElement.getBoundingClientRect().height - (this.horizontal ? 0 : squareLayoutPadding); const [pLeft, pRight, pTop, pBottom] = ['paddingLeft', 'paddingRight', 'paddingTop', 'paddingBottom']
.map(side => resolveCssSize(computedStyle[side])[0]);
const widgetBoundingClientRect = this.valueCardPanel.nativeElement.getBoundingClientRect();
const panelWidth = widgetBoundingClientRect.width - (pLeft + pRight);
const panelHeight = widgetBoundingClientRect.height - (pTop + pBottom);
let scale: number; let scale: number;
if (!this.horizontal) { if (!this.horizontal) {
const size = Math.min(panelWidth, panelHeight); const size = Math.min(panelWidth, panelHeight);

10
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts

@ -19,8 +19,10 @@ import {
BackgroundType, BackgroundType,
ColorSettings, ColorSettings,
constantColor, constantColor,
cssUnit, DateFormatSettings, cssUnit,
Font, lastUpdateAgoDateFormat DateFormatSettings,
Font,
lastUpdateAgoDateFormat
} from '@shared/models/widget-settings.models'; } from '@shared/models/widget-settings.models';
export enum ValueCardLayout { export enum ValueCardLayout {
@ -80,6 +82,7 @@ export interface ValueCardWidgetSettings {
dateFont: Font; dateFont: Font;
dateColor: ColorSettings; dateColor: ColorSettings;
background: BackgroundSettings; background: BackgroundSettings;
padding: string;
} }
export const valueCardDefaultSettings = (horizontal: boolean): ValueCardWidgetSettings => ({ export const valueCardDefaultSettings = (horizontal: boolean): ValueCardWidgetSettings => ({
@ -128,5 +131,6 @@ export const valueCardDefaultSettings = (horizontal: boolean): ValueCardWidgetSe
color: 'rgba(255,255,255,0.72)', color: 'rgba(255,255,255,0.72)',
blur: 3 blur: 3
} }
} },
padding: ''
}); });

2
ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html

@ -20,7 +20,7 @@
<div class="tb-status-widget-title-panel"> <div class="tb-status-widget-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> <ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
</div> </div>
<div #statusWidgetContent class="tb-status-widget-content" [class]="this.layout"> <div #statusWidgetContent class="tb-status-widget-content" [class]="this.layout" [style.padding]="padding">
<div class="tb-status-widget-icon-container"> <div class="tb-status-widget-icon-container">
<tb-icon [style]="iconStyle">{{ icon }}</tb-icon> <tb-icon [style]="iconStyle">{{ icon }}</tb-icon>
</div> </div>

25
ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts

@ -21,14 +21,16 @@ import {
ElementRef, ElementRef,
OnDestroy, OnDestroy,
OnInit, OnInit,
Renderer2, ViewChild, Renderer2,
ViewChild,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models'; import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models';
import { import {
statusWidgetDefaultSettings, statusWidgetDefaultSettings,
StatusWidgetLayout, StatusWidgetLayout,
StatusWidgetSettings, StatusWidgetStateSettings StatusWidgetSettings,
StatusWidgetStateSettings
} from '@home/components/widget/lib/indicator/status-widget.models'; } from '@home/components/widget/lib/indicator/status-widget.models';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { import {
@ -36,12 +38,12 @@ import {
ComponentStyle, ComponentStyle,
iconStyle, iconStyle,
overlayStyle, overlayStyle,
resolveCssSize,
textStyle textStyle
} from '@shared/models/widget-settings.models'; } from '@shared/models/widget-settings.models';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { UtilsService } from '@core/services/utils.service';
import { ValueType } from '@shared/models/constants'; import { ValueType } from '@shared/models/constants';
const initialStatusWidgetSize = 147; const initialStatusWidgetSize = 147;
@ -65,6 +67,7 @@ export class StatusWidgetComponent extends
backgroundStyle$: Observable<ComponentStyle>; backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {}; overlayStyle: ComponentStyle = {};
padding: string;
overlayInset = '12px'; overlayInset = '12px';
borderRadius = ''; borderRadius = '';
@ -101,9 +104,7 @@ export class StatusWidgetComponent extends
constructor(protected imagePipe: ImagePipe, constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer, protected sanitizer: DomSanitizer,
private renderer: Renderer2, private renderer: Renderer2,
private utils: UtilsService, protected cd: ChangeDetectorRef) {
protected cd: ChangeDetectorRef,
private elementRef: ElementRef) {
super(cd); super(cd);
} }
@ -191,8 +192,13 @@ export class StatusWidgetComponent extends
} }
private onResize() { private onResize() {
const panelWidth = this.statusWidgetPanel.nativeElement.getBoundingClientRect().width; const computedStyle = getComputedStyle(this.statusWidgetPanel.nativeElement);
const panelHeight = this.statusWidgetPanel.nativeElement.getBoundingClientRect().height; const [pLeft, pRight, pTop, pBottom] = ['paddingLeft', 'paddingRight', 'paddingTop', 'paddingBottom']
.map(side => resolveCssSize(computedStyle[side])[0]);
const widgetBoundingClientRect = this.statusWidgetPanel.nativeElement.getBoundingClientRect();
const panelWidth = widgetBoundingClientRect.width - (pLeft + pRight);
const panelHeight = widgetBoundingClientRect.height - (pTop + pBottom);
const targetSize = Math.min(panelWidth, panelHeight); const targetSize = Math.min(panelWidth, panelHeight);
const scale = targetSize / initialStatusWidgetSize; const scale = targetSize / initialStatusWidgetSize;
const width = initialStatusWidgetSize; const width = initialStatusWidgetSize;
@ -220,6 +226,9 @@ export class StatusWidgetComponent extends
this.showLabel = stateSettings.showLabel && this.layout !== StatusWidgetLayout.icon; this.showLabel = stateSettings.showLabel && this.layout !== StatusWidgetLayout.icon;
this.showStatus = stateSettings.showStatus && this.layout !== StatusWidgetLayout.icon; this.showStatus = stateSettings.showStatus && this.layout !== StatusWidgetLayout.icon;
this.icon = stateSettings.icon; this.icon = stateSettings.icon;
this.padding = stateSettings.backgroundDisabled.overlay.enabled || stateSettings.background.overlay.enabled
? undefined
: this.settings.padding;
const primaryColor = disabled ? stateSettings.primaryColorDisabled : stateSettings.primaryColor; const primaryColor = disabled ? stateSettings.primaryColorDisabled : stateSettings.primaryColor;
const secondaryColor = disabled ? stateSettings.secondaryColorDisabled : stateSettings.secondaryColor; const secondaryColor = disabled ? stateSettings.secondaryColorDisabled : stateSettings.secondaryColor;

4
ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.models.ts

@ -65,6 +65,7 @@ export interface StatusWidgetSettings {
layout: StatusWidgetLayout; layout: StatusWidgetLayout;
onState: StatusWidgetStateSettings; onState: StatusWidgetStateSettings;
offState: StatusWidgetStateSettings; offState: StatusWidgetStateSettings;
padding: string
} }
export const statusWidgetDefaultSettings: StatusWidgetSettings = { export const statusWidgetDefaultSettings: StatusWidgetSettings = {
@ -200,5 +201,6 @@ export const statusWidgetDefaultSettings: StatusWidgetSettings = {
blur: 3 blur: 3
} }
} }
} },
padding: ''
}; };

2
ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div #singleSwitchPanel class="tb-single-switch-panel" [class.auto-scale]="autoScale" [style.pointer-events]="ctx.isEdit ? 'none' : 'all'" [style]="backgroundStyle$ | async"> <div #singleSwitchPanel class="tb-single-switch-panel" [class.auto-scale]="autoScale" [style.pointer-events]="ctx.isEdit ? 'none' : 'all'" [style.padding]="padding" [style]="backgroundStyle$ | async">
<div class="tb-single-switch-overlay" [style]="overlayStyle" [style.inset]="overlayInset"></div> <div class="tb-single-switch-overlay" [style]="overlayStyle" [style.inset]="overlayInset"></div>
<div class="tb-single-switch-title-panel"> <div class="tb-single-switch-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> <ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>

13
ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts

@ -36,6 +36,7 @@ import {
ComponentStyle, ComponentStyle,
iconStyle, iconStyle,
overlayStyle, overlayStyle,
resolveCssSize,
textStyle textStyle
} from '@shared/models/widget-settings.models'; } from '@shared/models/widget-settings.models';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@ -74,6 +75,7 @@ export class SingleSwitchWidgetComponent extends
backgroundStyle$: Observable<ComponentStyle>; backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {}; overlayStyle: ComponentStyle = {};
padding: string;
overlayInset = '12px'; overlayInset = '12px';
value = false; value = false;
@ -123,6 +125,7 @@ export class SingleSwitchWidgetComponent extends
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay); this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding;
this.layout = this.settings.layout; this.layout = this.settings.layout;
@ -231,11 +234,15 @@ export class SingleSwitchWidgetComponent extends
} }
private onResize() { private onResize() {
const height = this.singleSwitchPanel.nativeElement.getBoundingClientRect().height; const widgetBoundingClientRect = this.singleSwitchPanel.nativeElement.getBoundingClientRect();
const height = widgetBoundingClientRect.height;
const switchScale = height / initialSwitchHeight; const switchScale = height / initialSwitchHeight;
const paddingScale = Math.min(switchScale, 1); const paddingScale = Math.min(switchScale, 1);
const panelWidth = this.singleSwitchPanel.nativeElement.getBoundingClientRect().width - (horizontalLayoutPadding * paddingScale); const computedStyle = getComputedStyle(this.singleSwitchPanel.nativeElement);
const panelHeight = this.singleSwitchPanel.nativeElement.getBoundingClientRect().height - (verticalLayoutPadding * paddingScale); const [pLeft, pRight, pTop, pBottom] = ['paddingLeft', 'paddingRight', 'paddingTop', 'paddingBottom']
.map(side => resolveCssSize(computedStyle[side])[0]);
const panelWidth = widgetBoundingClientRect.width - ((pLeft + pRight) || horizontalLayoutPadding * paddingScale);
const panelHeight = widgetBoundingClientRect.height - ((pTop + pBottom) || verticalLayoutPadding * paddingScale);
this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'transform', `scale(1)`); this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'transform', `scale(1)`);
this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'width', 'auto'); this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'width', 'auto');
let contentWidth = this.singleSwitchToggleRow.nativeElement.getBoundingClientRect().width; let contentWidth = this.singleSwitchToggleRow.nativeElement.getBoundingClientRect().width;

4
ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.models.ts

@ -80,6 +80,7 @@ export interface SingleSwitchWidgetSettings {
offLabelFont: Font; offLabelFont: Font;
offLabelColor: string; offLabelColor: string;
background: BackgroundSettings; background: BackgroundSettings;
padding: string;
} }
export const singleSwitchDefaultSettings: SingleSwitchWidgetSettings = { export const singleSwitchDefaultSettings: SingleSwitchWidgetSettings = {
@ -217,5 +218,6 @@ export const singleSwitchDefaultSettings: SingleSwitchWidgetSettings = {
color: 'rgba(255,255,255,0.72)', color: 'rgba(255,255,255,0.72)',
blur: 3 blur: 3
} }
} },
padding: ''
}; };

18
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts

@ -191,10 +191,10 @@ export interface ScadaSymbolMetadata {
properties: ScadaSymbolProperty[]; properties: ScadaSymbolProperty[];
} }
export const emptyMetadata = (): ScadaSymbolMetadata => ({ export const emptyMetadata = (width?: number, height?: number): ScadaSymbolMetadata => ({
title: '', title: '',
widgetSizeX: 3, widgetSizeX: width ? width/100 : 3,
widgetSizeY: 3, widgetSizeY: height ? height/100 : 3,
tags: [], tags: [],
behavior: [], behavior: [],
properties: [] properties: []
@ -255,7 +255,17 @@ const parseScadaSymbolMetadataFromDom = (svgDoc: Document): ScadaSymbolMetadata
if (elements.length) { if (elements.length) {
return JSON.parse(elements[0].textContent); return JSON.parse(elements[0].textContent);
} else { } else {
return emptyMetadata(); const svg = svgDoc.getElementsByTagName('svg')[0];
let width = null;
let height = null;
if (svg.viewBox.baseVal.width && svg.viewBox.baseVal.height) {
width = svg.viewBox.baseVal.width;
height = svg.viewBox.baseVal.height;
} else if (svg.width.baseVal.value && svg.height.baseVal.value) {
width = svg.width.baseVal.value;
height = svg.height.baseVal.value;
}
return emptyMetadata(width, height);
} }
} catch (_e) { } catch (_e) {
console.error(_e); console.error(_e);

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

@ -93,5 +93,11 @@
<tb-background-settings formControlName="background"> <tb-background-settings formControlName="background">
</tb-background-settings> </tb-background-settings>
</div> </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> </div>
</ng-container> </ng-container>

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.ts

@ -110,7 +110,8 @@ export class ValueCardWidgetSettingsComponent extends WidgetSettingsComponent {
dateFont: [settings.dateFont, []], dateFont: [settings.dateFont, []],
dateColor: [settings.dateColor, []], dateColor: [settings.dateColor, []],
background: [settings.background, []] background: [settings.background, []],
padding: [settings.padding, []]
}); });
} }

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html

@ -123,6 +123,12 @@
<tb-background-settings formControlName="background"> <tb-background-settings formControlName="background">
</tb-background-settings> </tb-background-settings>
</div> </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> </div>
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.single-switch.switch</div> <div class="tb-form-panel-title" translate>widgets.single-switch.switch</div>

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts

@ -102,7 +102,8 @@ export class SingleSwitchWidgetSettingsComponent extends WidgetSettingsComponent
offLabelFont: [settings.offLabelFont, []], offLabelFont: [settings.offLabelFont, []],
offLabelColor: [settings.offLabelColor, []], offLabelColor: [settings.offLabelColor, []],
background: [settings.background, []] background: [settings.background, []],
padding: [settings.padding, []]
}); });
} }

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

@ -78,5 +78,11 @@
[layout]="statusWidgetSettingsForm.get('layout').value" [layout]="statusWidgetSettingsForm.get('layout').value"
formControlName="offState"> formControlName="offState">
</tb-status-widget-state-settings> </tb-status-widget-state-settings>
<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> </div>
</ng-container> </ng-container>

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.ts

@ -73,7 +73,8 @@ export class StatusWidgetSettingsComponent extends WidgetSettingsComponent {
disabledState: [settings.disabledState, []], disabledState: [settings.disabledState, []],
layout: [settings.layout, []], layout: [settings.layout, []],
onState: [settings.onState, []], onState: [settings.onState, []],
offState: [settings.offState, []] offState: [settings.offState, []],
padding: [settings.padding, []]
}); });
} }
} }

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html

@ -100,5 +100,11 @@
<tb-background-settings formControlName="background"> <tb-background-settings formControlName="background">
</tb-background-settings> </tb-background-settings>
</div> </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> </div>
</ng-container> </ng-container>

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.ts

@ -91,7 +91,8 @@ export class WindSpeedDirectionWidgetSettingsComponent extends WidgetSettingsCom
arrowColor: [settings.arrowColor, []], arrowColor: [settings.arrowColor, []],
background: [settings.background, []] background: [settings.background, []],
padding: [settings.padding, []]
}); });
} }

2
ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-wind-speed-direction-panel" [style]="backgroundStyle$ | async" [class.tb-wind-speed-direction-pointer]="hasCardClickAction" (click)="cardClick($event)"> <div class="tb-wind-speed-direction-panel" [style.padding]="padding" [style]="backgroundStyle$ | async" [class.tb-wind-speed-direction-pointer]="hasCardClickAction" (click)="cardClick($event)">
<div class="tb-wind-speed-direction-overlay" [style]="overlayStyle"></div> <div class="tb-wind-speed-direction-overlay" [style]="overlayStyle"></div>
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> <ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div class="tb-wind-speed-direction-content"> <div class="tb-wind-speed-direction-content">

6
ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.ts

@ -42,7 +42,6 @@ import {
getSingleTsValueByDataKey, getSingleTsValueByDataKey,
overlayStyle overlayStyle
} from '@shared/models/widget-settings.models'; } from '@shared/models/widget-settings.models';
import { WidgetComponent } from '@home/components/widget/widget.component';
import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils'; import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { Path, Svg, SVG, Text } from '@svgdotjs/svg.js'; import { Path, Svg, SVG, Text } from '@svgdotjs/svg.js';
@ -92,6 +91,7 @@ export class WindSpeedDirectionWidgetComponent implements OnInit, OnDestroy, Aft
backgroundStyle$: Observable<ComponentStyle>; backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {}; overlayStyle: ComponentStyle = {};
padding: string;
shapeResize$: ResizeObserver; shapeResize$: ResizeObserver;
@ -111,8 +111,7 @@ export class WindSpeedDirectionWidgetComponent implements OnInit, OnDestroy, Aft
private windDirection = 0; private windDirection = 0;
private centerValueText = 'N/A'; private centerValueText = 'N/A';
constructor(private widgetComponent: WidgetComponent, constructor(private imagePipe: ImagePipe,
private imagePipe: ImagePipe,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private renderer: Renderer2, private renderer: Renderer2,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef) {
@ -142,6 +141,7 @@ export class WindSpeedDirectionWidgetComponent implements OnInit, OnDestroy, Aft
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay); this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding;
this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0; this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0;
} }

5
ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.models.ts

@ -14,7 +14,6 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { BatteryLevelLayout } from '@home/components/widget/lib/indicator/battery-level-widget.models';
import { import {
BackgroundSettings, BackgroundSettings,
BackgroundType, BackgroundType,
@ -59,6 +58,7 @@ export interface WindSpeedDirectionWidgetSettings {
minorTicksColor: string; minorTicksColor: string;
minorTicksFont: Font; minorTicksFont: Font;
background: BackgroundSettings; background: BackgroundSettings;
padding: string
} }
export const windSpeedDirectionDefaultSettings: WindSpeedDirectionWidgetSettings = { export const windSpeedDirectionDefaultSettings: WindSpeedDirectionWidgetSettings = {
@ -101,5 +101,6 @@ export const windSpeedDirectionDefaultSettings: WindSpeedDirectionWidgetSettings
color: 'rgba(255,255,255,0.72)', color: 'rgba(255,255,255,0.72)',
blur: 3 blur: 3
} }
} },
padding: ''
}; };

5
ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts

@ -381,9 +381,8 @@ export class WidgetComponentService {
widgetInfo.templateHtml, widgetInfo.templateHtml,
resolvedModules.modules resolvedModules.modules
).pipe( ).pipe(
map((componentData) => { map((componentType) => {
widgetInfo.componentType = componentData.componentType; widgetInfo.componentType = componentType;
widgetInfo.componentModuleRef = componentData.componentModuleRef;
return null; return null;
}), }),
catchError(e => { catchError(e => {

2
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -760,7 +760,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
try { try {
this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentType, this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentType,
{index: 0, injector, ngModuleRef: this.widgetInfo.componentModuleRef}); {index: 0, injector});
this.cd.detectChanges(); this.cd.detectChanges();
} catch (e) { } catch (e) {
if (this.dynamicWidgetComponentRef) { if (this.dynamicWidgetComponentRef) {

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

Loading…
Cancel
Save