Browse Source

Merge branch 'master' into fix_bug_lwm2m_Composite_Observer_Update

pull/11597/head
nick 2 years ago
parent
commit
69e7bbc74a
  1. 158
      application/src/main/data/json/system/scada_symbols/leak-sensor.svg
  2. 733
      application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg
  3. 733
      application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg
  4. 23
      application/src/main/data/json/system/scada_symbols/sand-filter.svg
  5. 4
      application/src/main/data/json/system/scada_symbols/vertical-ball-valve.svg
  6. 276
      application/src/main/data/json/system/scada_symbols/waterstop.svg
  7. 74
      application/src/main/data/json/system/widget_bundles/scada_fluid_system.json
  8. 2
      application/src/main/data/json/system/widget_bundles/scada_symbols.json
  9. 70
      application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json
  10. 2
      application/src/main/data/json/system/widget_types/scada_symbol.json
  11. 19
      application/src/main/data/upgrade/3.7.0/schema_update.sql
  12. 11
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  13. 6
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  14. 126
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  15. 25
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  16. 2
      application/src/main/java/org/thingsboard/server/controller/ImageController.java
  17. 3
      application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java
  18. 1
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  19. 69
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  20. 13
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  21. 2
      application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java
  22. 32
      application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java
  23. 5
      application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java
  24. 16
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  25. 1
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
  26. 11
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  27. 2
      application/src/main/java/org/thingsboard/server/service/mail/DefaultTbMailConfigTemplateService.java
  28. 10
      application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java
  29. 179
      application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java
  30. 93
      application/src/main/java/org/thingsboard/server/service/notification/channels/TeamsAdaptiveCard.java
  31. 92
      application/src/main/java/org/thingsboard/server/service/notification/channels/TeamsMessageCard.java
  32. 8
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  33. 82
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  34. 6
      application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java
  35. 10
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  36. 6
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  37. 5
      application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
  38. 2
      application/src/main/resources/templates/activation.ftl
  39. 2
      application/src/main/resources/templates/reset.password.ftl
  40. 6
      application/src/main/resources/thingsboard.yml
  41. 5
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  42. 184
      application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java
  43. 4
      application/src/test/java/org/thingsboard/server/controller/DomainControllerTest.java
  44. 4
      application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java
  45. 25
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  46. 18
      application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
  47. 12
      application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java
  48. 2
      application/src/test/java/org/thingsboard/server/service/device/provision/DeviceProvisionServiceTest.java
  49. 86
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  50. 59
      application/src/test/java/org/thingsboard/server/transport/coap/provision/CoapProvisionJsonDeviceTest.java
  51. 44
      application/src/test/java/org/thingsboard/server/transport/coap/provision/CoapProvisionProtoDeviceTest.java
  52. 58
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionJsonDeviceTest.java
  53. 45
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionProtoDeviceTest.java
  54. 148
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/MqttClientSideRpcIntegrationTest.java
  55. 2
      common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java
  56. 5
      common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java
  57. 2
      common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java
  58. 16
      common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java
  59. 8
      common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java
  60. 26
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedCacheKey.java
  61. 2
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java
  62. 19
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java
  63. 2
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbCache.java
  64. 9
      common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java
  65. 1
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionRequest.java
  66. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  67. 19
      common/data/src/main/java/org/thingsboard/server/common/data/UserActivationLink.java
  68. 1
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java
  69. 104
      common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java
  70. 25
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java
  71. 1
      common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java
  72. 1
      common/proto/src/main/proto/queue.proto
  73. 32
      common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplate.java
  74. 2
      common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqSettings.java
  75. 19
      common/queue/src/main/java/org/thingsboard/server/queue/scheduler/DefaultSchedulerComponent.java
  76. 128
      common/queue/src/test/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplateTest.java
  77. 95
      common/queue/src/test/java/org/thingsboard/server/queue/scheduler/DefaultSchedulerComponentTest.java
  78. 37
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  79. 94
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  80. 4
      common/transport/coap/src/main/proto/efento/proto_config.proto
  81. 5
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java
  82. 50
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  83. 25
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/limits/GatewaySessionLimits.java
  84. 29
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/limits/SessionLimits.java
  85. 1
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java
  86. 97
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/AbstractActivityManager.java
  87. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/FirstAndLastEventActivityStrategy.java
  88. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/FirstEventActivityStrategy.java
  89. 5
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java
  90. 10
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  91. 9
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCacheKey.java
  92. 7
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java
  93. 9
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCacheKey.java
  94. 9
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java
  95. 7
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
  96. 7
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  97. 2
      dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java
  98. 4
      dao/src/main/java/org/thingsboard/server/dao/edge/BaseRelatedEdgesService.java
  99. 3
      dao/src/main/java/org/thingsboard/server/dao/entity/CachedVersionedEntityService.java
  100. 9
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java

158
application/src/main/data/json/system/scada_symbols/leak-sensor.svg

@ -0,0 +1,158 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="100" height="100" fill="none" version="1.1" viewBox="0 0 100 100">
<tb:metadata xmlns=""><![CDATA[{
"title": "Leak sensor",
"description": "Leak sensor",
"searchTags": [
"leak"
],
"widgetSizeX": 1,
"widgetSizeY": 1,
"tags": [
{
"tag": "background",
"stateRenderFunction": "var fill = ctx.properties.backgroundColor;\nelement.attr({fill: fill});",
"actions": null
},
{
"tag": "clickArea",
"stateRenderFunction": null,
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'click');"
}
}
},
{
"tag": "icon",
"stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.attr({stroke: ctx.properties.leakColor});\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n} else {\n element.attr({stroke: ctx.properties.defaultColor});\n ctx.api.resetAnimation(element);\n}\n",
"actions": null
}
],
"behavior": [
{
"id": "leak",
"name": "{i18n:scada.symbol.leak}",
"hint": "{i18n:scada.symbol.water-leak-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "GET_TIME_SERIES",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "leak"
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "click",
"name": "{i18n:scada.symbol.on-click}",
"hint": "{i18n:scada.symbol.on-click-hint}",
"group": null,
"type": "widgetAction",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": null,
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": {
"type": "doNothing",
"targetDashboardStateId": null,
"openRightLayout": false,
"setEntityId": false,
"stateEntityParamName": null
}
}
],
"properties": [
{
"id": "backgroundColor",
"name": "{i18n:scada.symbol.background-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
},
{
"id": "defaultColor",
"name": "{i18n:scada.symbol.default-color}",
"type": "color",
"default": "#1C943E",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
},
{
"id": "leakColor",
"name": "{i18n:scada.symbol.leak-color}",
"type": "color",
"default": "#D12730",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
]
}]]></tb:metadata><rect x=".5" y=".5" width="99" height="99" rx="23.5" fill="#fff" tb:tag="background"/><rect x=".5" y=".5" width="99" height="99" rx="23.5" fill="url(#paint0_linear_2089_217648)"/><rect x=".5" y=".5" width="99" height="99" rx="23.5" fill="url(#paint1_linear_2089_217648)"/><rect x=".5" y=".5" width="99" height="99" rx="23.5" stroke="#fff"/><path d="m56.668 52.309c0 2.04-0.7902 3.9966-2.1967 5.4391-0.7985 0.819-1.7586 1.4347-2.8035 1.8135m10-6.7401c0-10.962-11.667-19.487-11.667-19.487s-11.667 8.5256-11.667 19.487c0 3.2301 1.2292 6.3283 3.4171 8.6125 2.1879 2.284 5.1556 3.5659 8.2497 3.5659 3.0942 0 6.0615-1.2826 8.2493-3.5666 2.188-2.2842 3.4172-5.3817 3.4172-8.6118z" stroke="#1C943E" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" tb:tag="icon"/><path d="m33.631 0s-33.631 0-33.631 16.75v82.09c0 0.6628 0.89543 1.1601 2 1.1601h96c1.1045 0 2-0.4973 2-1.1601v-82.09c0-16.75-33.035-16.75-33.035-16.75h-16.965zm33.535 20.3c-0.64434 0-1.1666 0.3134-1.1666 0.7v75.1c0 0.3866 0.52234 0.7 1.1666 0.7h7.3336c0.64434 0 1.1666-0.3134 1.1666-0.7v-75.1c0-0.3866-0.52233-0.7-1.1666-0.7z" fill="#000" fill-opacity="0" style="stroke-width:.059761" tb:tag="clickArea"/><defs>
<linearGradient id="paint0_linear_2089_217648" x1="100" x2="1.5002" y1="50.119" y2="49.881" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202" stop-opacity=".2" offset="0"/>
<stop stop-color="#020202" stop-opacity=".12" offset=".05"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".15"/>
<stop stop-color="#fff" stop-opacity=".2" offset=".51114"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".85"/>
<stop stop-color="#020202" stop-opacity=".12" offset=".95"/>
<stop stop-color="#020202" stop-opacity=".2" offset=".99341"/>
</linearGradient>
<linearGradient id="paint1_linear_2089_217648" x1="50" x2="50" y2="100" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202" stop-opacity=".2" offset=".01"/>
<stop stop-color="#020202" stop-opacity=".12" offset=".05"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".15343"/>
<stop stop-color="#fff" stop-opacity=".2" offset=".5"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".85"/>
<stop stop-opacity=".12" offset=".95"/>
<stop stop-opacity=".2" offset="1"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 56 KiB

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 56 KiB

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

@ -1,13 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="600" height="1e3" fill="none" version="1.1" viewBox="0 0 600 1e3">
<tb:metadata xmlns=""><![CDATA[{
"title": "Stand filter",
"description": "Stand filter with configurable filtration mode option and various states.",
"title": "Sand filter",
"description": "Sand filter with configurable filtration mode option and various states.",
"searchTags": [
"filter"
"filter",
"sand"
],
"widgetSizeX": 3,
"widgetSizeY": 5,
"stateRenderFunction": "",
"stateRenderFunction": "var running = ctx.values.running;\nif (running) {\n ctx.api.enable(svg.children());\n} else {\n ctx.api.disable(svg.children());\n}",
"tags": [
{
"tag": "background",
@ -25,7 +26,7 @@
},
{
"tag": "filterMode",
"stateRenderFunction": "var defaultBorderColor = ctx.properties.defaultBorderColor;\nvar activeBorderColor = ctx.properties.activeBorderColor;\nvar defaultLabelColor = ctx.properties.defaultLabelColor;\nvar activeLabelColor = ctx.properties.activeLabelColor;\nvar boxBackground = ctx.properties.modeBoxBackground;\n\nvar running = ctx.values.running;\nvar filtrationMode = ctx.values.filtrationMode;\n\nvar filtrationMap = {};\nvar bottomShift = 104;\nvar rightShift = 226;\nvar middleShift = 121;\n\nvar filterModeSet = element.remember('filterModeSet');\n\nif (!filterModeSet) {\n element.remember('filterModeSet', true);\n var clone = element.children()[0];\n setFilterModeColors(clone);\n element.clear();\n \n var i = 0;\n if (ctx.properties.filtrationMode) {\n i++;\n filtrationMap[i] = 'filter';\n }\n if (ctx.properties.wasteMode) {\n i++;\n filtrationMap[i] = 'waste';\n }\n if (ctx.properties.backwashMode) {\n i++;\n filtrationMap[i] = 'backwash';\n }\n if (ctx.properties.recirculateMode) {\n i++;\n filtrationMap[i] = 'recirculate';\n }\n if (ctx.properties.rinseMode) {\n i++;\n filtrationMap[i] = 'rinse';\n }\n if (ctx.properties.closedMode) {\n i++;\n filtrationMap[i] = 'closed';\n }\n \n var filterMode = Object.values(filtrationMap);\n var lastToMiddle = filterMode.length % 2;\n \n filterMode.forEach((mode, index, arr) => {\n var template = clone.clone();\n var x = (index % 2) * rightShift;\n var y = Math.floor((index % filterMode.length) / 2) * bottomShift;\n if (index === filterMode.length-1 && lastToMiddle) {\n x = middleShift;\n }\n template.attr({'class': mode}).css('cursor', 'pointer').translate(x, y);\n ctx.api.text(template.findOne('text'), capitalizeFirstLetter(mode));\n template.click((event) => click(event, getFilterModeKey(mode)));\n element.add(template);\n })\n if (isFinite(filtrationMode)) {\n setFilterModeColorsByMap(filtrationMode, running);\n }\n}\n\nif (running) {\n ctx.api.enable(element.children());\n} else {\n ctx.api.disable(element.children());\n}\n\nfunction click(event, index) {\n var filtrationMode = ctx.values.filtrationMode;\n if (ctx.values.running && isFinite(filtrationMode)) {\n ctx.api.disable(element.children());\n var newValue = +index;\n if (newValue === filtrationMode) {\n newValue = 0;\n } else {\n setFilterModeColorsByMap(filtrationMode);\n }\n ctx.api.setValue('filtrationMode', newValue);\n ctx.api.callAction(event, 'filtrationModeUpdateState', newValue, {\n next: () => {\n setFilterModeColorsByMap(newValue ? newValue: filtrationMode, newValue);\n ctx.api.enable(element.children());\n },\n error: () => {\n setFilterModeColorsByMap(newValue);\n ctx.api.setValue('filtrationMode', filtrationMode);\n }\n });\n }\n}\n\nfunction getFilterModeKey(value) {\n return Object.keys(filtrationMap).find(key => filtrationMap[key] === value);\n}\n\nfunction setFilterModeColorsByMap(mode, active = false) {\n var filterBox = element.findOne('g.'+filtrationMap[mode])\n if (filterBox) {\n return setFilterModeColors(filterBox, active);\n }\n}\n\nfunction setFilterModeColors(filterBox, active = false) {\n if (filterBox) {\n var borderColor = active ? activeBorderColor : defaultBorderColor;\n var labelColor = active ? activeLabelColor : defaultLabelColor;\n filterBox.children()[0].fill(boxBackground);\n filterBox.children()[1].stroke(borderColor);\n filterBox.children()[2].fill(borderColor);\n ctx.api.font(filterBox.findOne('text'), null, labelColor);\n }\n}\n\nfunction capitalizeFirstLetter(string) {\n return string.charAt(0).toUpperCase() + string.slice(1);\n}",
"stateRenderFunction": "var defaultBorderColor = ctx.properties.defaultBorderColor;\nvar activeBorderColor = ctx.properties.activeBorderColor;\nvar defaultLabelColor = ctx.properties.defaultLabelColor;\nvar activeLabelColor = ctx.properties.activeLabelColor;\nvar boxBackground = ctx.properties.modeBoxBackground;\n\nvar running = ctx.values.running;\nvar filtrationMode = ctx.values.filtrationMode;\n\nvar filtrationMap = {};\nvar bottomShift = 104;\nvar rightShift = 226;\nvar middleShift = 121;\n\nvar filterModeSet = element.remember('filterModeSet');\n\nvar i = 0;\nif (ctx.properties.filtrationMode) {\n i++;\n filtrationMap[i] = 'filter';\n}\nif (ctx.properties.wasteMode) {\n i++;\n filtrationMap[i] = 'waste';\n}\nif (ctx.properties.backwashMode) {\n i++;\n filtrationMap[i] = 'backwash';\n}\nif (ctx.properties.recirculateMode) {\n i++;\n filtrationMap[i] = 'recirculate';\n}\nif (ctx.properties.rinseMode) {\n i++;\n filtrationMap[i] = 'rinse';\n}\nif (ctx.properties.closedMode) {\n i++;\n filtrationMap[i] = 'closed';\n}\n\nif (!filterModeSet) {\n element.remember('filterModeSet', true);\n var clone = element.children()[0];\n setFilterModeColors(clone);\n element.clear();\n \n var filterMode = Object.values(filtrationMap);\n var lastToMiddle = filterMode.length % 2;\n \n filterMode.forEach((mode, index, arr) => {\n var template = clone.clone();\n var x = (index % 2) * rightShift;\n var y = Math.floor((index % filterMode.length) / 2) * bottomShift;\n if (index === filterMode.length-1 && lastToMiddle) {\n x = middleShift;\n }\n template.attr({'class': mode}).css('cursor', 'pointer').translate(x, y);\n ctx.api.text(template.findOne('text'), capitalizeFirstLetter(mode));\n template.click((event) => click(event, getFilterModeKey(mode)));\n element.add(template);\n })\n}\n\nif (isFinite(filtrationMode)) {\n if (element.findOne('.active')) {\n setFilterModeColors(element.findOne('.active'));\n }\n setFilterModeColorsByMap(filtrationMode, running);\n}\n\nfunction click(event, index) {\n var filtrationMode = ctx.values.filtrationMode;\n if (ctx.values.running && isFinite(filtrationMode)) {\n ctx.api.disable(element.children());\n var newValue = +index;\n if (newValue === filtrationMode) {\n newValue = 0;\n } else {\n setFilterModeColorsByMap(filtrationMode);\n }\n ctx.api.setValue('filtrationMode', newValue);\n ctx.api.callAction(event, 'filtrationModeUpdateState', newValue, {\n next: () => {\n setFilterModeColorsByMap(newValue ? newValue: filtrationMode, newValue);\n ctx.api.enable(element.children());\n },\n error: () => {\n setFilterModeColorsByMap(newValue);\n ctx.api.setValue('filtrationMode', filtrationMode);\n ctx.api.enable(element.children());\n }\n });\n }\n}\n\nfunction getFilterModeKey(value) {\n return Object.keys(filtrationMap).find(key => filtrationMap[key] === value);\n}\n\nfunction setFilterModeColorsByMap(mode, active = false) {\n var filterBox = element.findOne('g.'+filtrationMap[mode])\n if (filterBox) {\n return setFilterModeColors(filterBox, active);\n }\n}\n\nfunction setFilterModeColors(filterBox, active = false) {\n if (filterBox) {\n if (active) {\n filterBox.addClass('active');\n } else {\n filterBox.removeClass('active');\n }\n var borderColor = active ? activeBorderColor : defaultBorderColor;\n var labelColor = active ? activeLabelColor : defaultLabelColor;\n filterBox.children()[0].fill(boxBackground);\n filterBox.children()[1].stroke(borderColor);\n filterBox.children()[2].fill(borderColor);\n ctx.api.font(filterBox.findOne('text'), null, labelColor);\n }\n}\n\nfunction capitalizeFirstLetter(string) {\n return string.charAt(0).toUpperCase() + string.slice(1);\n}",
"actions": null
}
],
@ -159,7 +160,7 @@
"id": "filtrationMode",
"name": "{i18n:scada.symbol.filter-mode}",
"type": "switch",
"default": false,
"default": true,
"required": null,
"subLabel": null,
"divider": null,
@ -175,7 +176,7 @@
"id": "wasteMode",
"name": "{i18n:scada.symbol.waste-mode}",
"type": "switch",
"default": false,
"default": true,
"required": null,
"subLabel": null,
"divider": null,
@ -191,7 +192,7 @@
"id": "backwashMode",
"name": "{i18n:scada.symbol.backwash-mode}",
"type": "switch",
"default": false,
"default": true,
"required": null,
"subLabel": null,
"divider": null,
@ -207,7 +208,7 @@
"id": "recirculateMode",
"name": "{i18n:scada.symbol.recirculate-mode}",
"type": "switch",
"default": false,
"default": true,
"required": null,
"subLabel": null,
"divider": null,
@ -223,7 +224,7 @@
"id": "rinseMode",
"name": "{i18n:scada.symbol.rinse-mode}",
"type": "switch",
"default": false,
"default": true,
"required": null,
"subLabel": null,
"divider": null,
@ -239,7 +240,7 @@
"id": "closedMode",
"name": "{i18n:scada.symbol.closed-mode}",
"type": "switch",
"default": false,
"default": true,
"required": null,
"subLabel": null,
"divider": null,

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

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

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

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

276
application/src/main/data/json/system/scada_symbols/waterstop.svg

@ -0,0 +1,276 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200">
<tb:metadata xmlns=""><![CDATA[{
"title": "Water stop",
"description": "Remotely controlled water shutoff valve with configurable connectors and various states.",
"searchTags": [
"water stop"
],
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "",
"tags": [
{
"tag": "clickArea",
"stateRenderFunction": null,
"actions": {
"click": {
"actionFunction": "var opened = ctx.values.opened;\nvar action = opened ? 'close' : 'open';\n\nctx.api.callAction(event, action, undefined, {\n next: () => {\n ctx.api.setValue('opened', !opened);\n }\n});"
}
}
},
{
"tag": "pipe-color",
"stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});",
"actions": null
},
{
"tag": "walve",
"stateRenderFunction": "var opened = ctx.values.opened;\nif (opened) {\n element.attr({fill: ctx.properties.openedColor});\n ctx.api.resetAnimation(element);\n} else {\n element.attr({fill: ctx.properties.closedColor});\n if (ctx.values.closeAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}\n",
"actions": null
}
],
"behavior": [
{
"id": "opened",
"name": "{i18n:scada.symbol.opened}",
"hint": "{i18n:scada.symbol.opened-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": "{i18n:scada.symbol.opened}",
"defaultGetValueSettings": {
"action": "GET_ATTRIBUTE",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": "SHARED_SCOPE",
"key": "open"
},
"getTimeSeries": {
"key": "state"
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "open",
"name": "{i18n:scada.symbol.open}",
"hint": "{i18n:scada.symbol.open-hint}",
"group": null,
"type": "action",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": null,
"defaultSetValueSettings": {
"action": "SET_ATTRIBUTE",
"executeRpc": {
"method": "setState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"setAttribute": {
"scope": "SHARED_SCOPE",
"key": "open"
},
"putTimeSeries": {
"key": "state"
},
"valueToData": {
"type": "CONSTANT",
"constantValue": false,
"valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
}
},
"defaultWidgetActionSettings": null
},
{
"id": "close",
"name": "{i18n:scada.symbol.close}",
"hint": "{i18n:scada.symbol.close-hint}",
"group": null,
"type": "action",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": null,
"defaultSetValueSettings": {
"action": "SET_ATTRIBUTE",
"executeRpc": {
"method": "setState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"setAttribute": {
"scope": "SHARED_SCOPE",
"key": "open"
},
"putTimeSeries": {
"key": "state"
},
"valueToData": {
"type": "CONSTANT",
"constantValue": true,
"valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
}
},
"defaultWidgetActionSettings": null
},
{
"id": "closeAnimation",
"name": "{i18n:scada.symbol.close-state-animation}",
"hint": "{i18n:scada.symbol.close-state-animation-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": "{i18n:scada.symbol.animation}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
}
],
"properties": [
{
"id": "openedColor",
"name": "{i18n:scada.symbol.opened-color}",
"type": "color",
"default": "#1C943E",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
},
{
"id": "closedColor",
"name": "{i18n:scada.symbol.closed-color}",
"type": "color",
"default": "#D12730",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
},
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
]
}]]></tb:metadata><path d="M14 64H186V136H14V64Z" fill="#fff" tb:tag="pipe-color"/><path d="M14 64H186V136H14V64Z" fill="url(#paint0_linear_2089_217683)"/><path d="m15.5 65.5h169v69h-169v-69z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="187.5" y="51.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/><rect x="1.5" y="51.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/><path d="m70.5 46c0 1.933 1.567 3.5 3.5 3.5l52 1e-4c1.933 0 3.5-1.567 3.5-3.5v-37c0-4.6944-3.806-8.5-8.5-8.5h-42c-4.6944 0-8.5 3.8056-8.5 8.5v37z" fill="#1C943E" tb:tag="walve"/><path d="m70.5 46c0 1.933 1.567 3.5 3.5 3.5l52 1e-4c1.933 0 3.5-1.567 3.5-3.5v-37c0-4.6944-3.806-8.5-8.5-8.5h-42c-4.6944 0-8.5 3.8056-8.5 8.5v37z" fill="url(#paint1_linear_2089_217683)"/><path d="m70.5 46c0 1.933 1.567 3.5 3.5 3.5l52 1e-4c1.933 0 3.5-1.567 3.5-3.5v-37c0-4.6944-3.806-8.5-8.5-8.5h-42c-4.6944 0-8.5 3.8056-8.5 8.5v37z" stroke="#fff"/><path d="m110 136v-72h-21v72h21z" fill="#1C943E" tb:tag="walve"/><path d="m110 136v-72h-21v72h21z" fill="url(#paint2_linear_2089_217683)"/><path d="m110 136v-72h-21v72h21z" fill="url(#paint3_linear_2089_217683)"/><path d="m109.5 135.5v-71h-20v71h20z" stroke="#000" stroke-opacity=".12"/><path d="m126 64c2.209 0 4-1.7908 4-3.9999v-6c0-2.2092-1.791-4.0001-4-4.0001h-52c-2.2091 0-4 1.7909-4 4v6c0 2.2091 1.7909 4 4 4h52z" fill="#647484"/><path d="m126 64c2.209 0 4-1.7908 4-3.9999v-6c0-2.2092-1.791-4.0001-4-4.0001h-52c-2.2091 0-4 1.7909-4 4v6c0 2.2091 1.7909 4 4 4h52z" fill="url(#paint4_linear_2089_217683)"/><path d="m126 62.5c1.381 0 2.5-1.1193 2.5-2.5v-6c0-1.3807-1.119-2.5-2.5-2.5h-52c-1.3807 0-2.5 1.1193-2.5 2.5v6c0 1.3807 1.1193 2.5 2.5 2.5h52z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m67.262 0s-67.262 0-67.262 33.5v164.18c0 1.3256 1.7909 2.3202 4 2.3202h192c2.209 0 4-0.9946 4-2.3202v-164.18c0-33.5-66.07-33.5-66.07-33.5h-33.93zm67.07 40.6c-1.2887 0-2.3332 0.6268-2.3332 1.4v150.2c0 0.7732 1.0447 1.4 2.3332 1.4h14.667c1.2887 0 2.3332-0.6268 2.3332-1.4v-150.2c0-0.7732-1.0447-1.4-2.3332-1.4z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs>
<linearGradient id="paint0_linear_2089_217683" x1="58.72" x2="58.53" y1="64" y2="136" gradientUnits="userSpaceOnUse">
<stop stop-color="#727171" offset="0"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".26388"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".41759"/>
<stop stop-color="#fff" stop-opacity="0" offset=".49829"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".58094"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".71855"/>
<stop stop-color="#727171" offset="1"/>
</linearGradient>
<linearGradient id="paint1_linear_2089_217683" x1="134.3" x2="65.705" y1="8.0405" y2="9.0306" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202" stop-opacity=".35" offset="0"/>
<stop stop-color="#020202" stop-opacity=".12" offset=".14093"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".39937"/>
<stop stop-color="#fff" stop-opacity=".3" offset=".49829"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".59721"/>
<stop stop-color="#020202" stop-opacity=".12" offset=".85946"/>
<stop stop-color="#020202" stop-opacity=".35" offset="1"/>
</linearGradient>
<linearGradient id="paint2_linear_2089_217683" x1="99.475" x2="100.06" y1="64" y2="134.92" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202" stop-opacity=".35" offset="0"/>
<stop stop-color="#020202" stop-opacity=".15" offset=".15823"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".28193"/>
<stop stop-color="#fff" stop-opacity=".2" offset=".50291"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".72128"/>
<stop stop-color="#020202" stop-opacity=".15" offset=".8423"/>
<stop stop-color="#020202" stop-opacity=".35" offset=".99341"/>
</linearGradient>
<linearGradient id="paint3_linear_2089_217683" x1="110" x2="89" y1="100" y2="100" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202" stop-opacity=".2" offset=".01"/>
<stop stop-color="#020202" stop-opacity=".12" offset=".05"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".15343"/>
<stop stop-color="#fff" stop-opacity=".2" offset=".5"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".85"/>
<stop stop-opacity=".12" offset=".95"/>
<stop stop-opacity=".2" offset="1"/>
</linearGradient>
<linearGradient id="paint4_linear_2089_217683" x1="72.287" x2="127.11" y1="55.78" y2="57.142" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202" stop-opacity=".2" offset="0"/>
<stop stop-color="#020202" stop-opacity=".18" offset=".090959"/>
<stop stop-color="#020202" stop-opacity=".1" offset=".1"/>
<stop stop-color="#020202" stop-opacity=".08" offset=".20513"/>
<stop stop-color="#020202" stop-opacity=".05" offset=".21555"/>
<stop stop-color="#020202" stop-opacity=".02" offset=".36962"/>
<stop stop-color="#fff" stop-opacity=".01" offset=".37768"/>
<stop stop-color="#fff" stop-opacity=".01" offset=".62413"/>
<stop stop-color="#020202" stop-opacity=".02" offset=".6313"/>
<stop stop-color="#020202" stop-opacity=".05" offset=".77601"/>
<stop stop-color="#020202" stop-opacity=".08" offset=".7898"/>
<stop stop-color="#020202" stop-opacity=".1" offset=".9"/>
<stop stop-color="#020202" stop-opacity=".18" offset=".90846"/>
<stop stop-color="#020202" stop-opacity=".2" offset="1"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 12 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

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

@ -1,70 +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",
"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",
"vertical_tank",
"stand_vertical_tank",
"cylindrical_tank",
"stand_cylindrical_tank",
"vertical_short_tank",
"stand_vertical_short_tank",
"large_cylindrical_tank",
"large_stand_cylindrical_tank",
"large_vertical_tank",
"large_stand_vertical_tank",
"horizontal_tank",
"stand_horizontal_tank",
"spherical_tank",
"small_spherical_tank",
"elevated_tank",
"pool"
]
}

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

File diff suppressed because one or more lines are too long

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

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

11
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java

@ -43,6 +43,7 @@ import org.thingsboard.server.gen.transport.TransportProtos;
@Slf4j
public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> {
private static final String UNKNOWN_NAME = "Unknown";
private final String ruleChainName;
private final TbApiUsageReportClient apiUsageClient;
private final DefaultTbContext defaultCtx;
@ -57,7 +58,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
this.ruleChainName = ruleChainName;
this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
this.defaultCtx = new DefaultTbContext(systemContext, ruleChainName, new RuleNodeCtx(tenantId, parent, self, ruleNode));
this.info = new RuleNodeInfo(ruleNodeId, ruleChainName, ruleNode != null ? ruleNode.getName() : "Unknown");
this.info = new RuleNodeInfo(ruleNodeId, ruleChainName, getName(ruleNode));
}
@Override
@ -75,7 +76,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
public void onUpdate(TbActorCtx context) throws Exception {
RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
if (isMyNodePartition(newRuleNode)) {
this.info = new RuleNodeInfo(entityId, ruleChainName, newRuleNode != null ? newRuleNode.getName() : "Unknown");
this.info = new RuleNodeInfo(entityId, ruleChainName, getName(newRuleNode));
boolean restartRequired = state != ComponentLifecycleState.ACTIVE ||
!(ruleNode.getType().equals(newRuleNode.getType()) && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
this.ruleNode = newRuleNode;
@ -167,7 +168,11 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
@Override
public String getComponentName() {
return ruleNode.getName();
return getName(ruleNode);
}
private String getName(RuleNode ruleNode) {
return ruleNode != null ? ruleNode.getName() : UNKNOWN_NAME;
}
private TbNode initComponent(RuleNode ruleNode) throws Exception {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

16
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -122,7 +122,21 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
updateSchema("3.6.4", 3006004, "3.7.0", 3007000, null);
break;
case "3.7.0":
updateSchema("3.7.0", 3007000, "3.7.1", 3007001, null);
updateSchema("3.7.0", 3007000, "3.7.1", 3007001, connection -> {
try {
connection.createStatement().execute("UPDATE rule_node SET " +
"configuration = CASE " +
" WHEN (configuration::jsonb ->> 'persistAlarmRulesState') = 'false'" +
" THEN (configuration::jsonb || '{\"fetchAlarmRulesStateOnStart\": \"false\"}'::jsonb)::varchar " +
" ELSE configuration " +
"END, " +
"configuration_version = 1 " +
"WHERE type = 'org.thingsboard.rule.engine.profile.TbDeviceProfileNode' " +
"AND configuration_version < 1;");
} catch (Exception e) {
log.warn("Failed to execute update script for device profile rule nodes due to: ", e);
}
});
break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);

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

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

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

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

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

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

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

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

179
application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java

@ -15,16 +15,17 @@
*/
package org.thingsboard.server.service.notification.channels;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.JacksonUtil;
@ -37,6 +38,8 @@ import org.thingsboard.server.common.data.notification.template.MicrosoftTeamsDe
import org.thingsboard.server.service.notification.NotificationProcessingContext;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.List;
@ -56,17 +59,93 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
@Override
public void sendNotification(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
Message message = new Message();
message.setThemeColor(Strings.emptyToNull(processedTemplate.getThemeColor()));
if (targetConfig.getUseOldApi() == null || Boolean.TRUE.equals(targetConfig.getUseOldApi())) {
sendTeamsMessageCard(targetConfig, processedTemplate, ctx);
} else {
sendTeamsAdaptiveCard(targetConfig, processedTemplate, ctx);
}
}
private void sendTeamsAdaptiveCard(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws URISyntaxException, JsonProcessingException {
TeamsAdaptiveCard teamsAdaptiveCard = new TeamsAdaptiveCard();
TeamsAdaptiveCard.Attachment attachment = new TeamsAdaptiveCard.Attachment();
teamsAdaptiveCard.setAttachments(List.of(attachment));
TeamsAdaptiveCard.AdaptiveCard adaptiveCard = new TeamsAdaptiveCard.AdaptiveCard();
attachment.setContent(adaptiveCard);
TeamsAdaptiveCard.BackgroundImage backgroundImage = new TeamsAdaptiveCard.BackgroundImage(processedTemplate.getThemeColor());
adaptiveCard.setBackgroundImage(backgroundImage);
if (StringUtils.isEmpty(processedTemplate.getSubject())) {
message.setText(processedTemplate.getBody());
TeamsAdaptiveCard.TextBlock textBlock = new TeamsAdaptiveCard.TextBlock();
textBlock.setText(processedTemplate.getBody());
textBlock.setWeight("Normal");
textBlock.setSize("Medium");
textBlock.setColor(processedTemplate.getThemeColor());
adaptiveCard.getTextBlocks().add(textBlock);
} else {
message.setSummary(processedTemplate.getSubject());
Message.Section section = new Message.Section();
TeamsAdaptiveCard.TextBlock subjectTextBlock = new TeamsAdaptiveCard.TextBlock();
subjectTextBlock.setText(processedTemplate.getSubject());
subjectTextBlock.setWeight("Bolder");
subjectTextBlock.setSize("Large");
subjectTextBlock.setColor(processedTemplate.getThemeColor());
adaptiveCard.getTextBlocks().add(subjectTextBlock);
TeamsAdaptiveCard.TextBlock bodyTextBlock = new TeamsAdaptiveCard.TextBlock();
bodyTextBlock.setText(processedTemplate.getBody());
bodyTextBlock.setWeight("Lighter");
bodyTextBlock.setSize("Medium");
bodyTextBlock.setColor(processedTemplate.getThemeColor());
adaptiveCard.getTextBlocks().add(bodyTextBlock);
}
String uri = getButtonUri(processedTemplate, ctx);
if (StringUtils.isNotBlank(uri) && processedTemplate.getButton().getText() != null) {
TeamsAdaptiveCard.ActionOpenUrl actionOpenUrl = new TeamsAdaptiveCard.ActionOpenUrl();
actionOpenUrl.setTitle(processedTemplate.getButton().getText());
actionOpenUrl.setUrl(uri);
adaptiveCard.getActions().add(actionOpenUrl);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity<>(JacksonUtil.toString(teamsAdaptiveCard), headers);
restTemplate.postForEntity(new URI(targetConfig.getWebhookUrl()), request, String.class);
}
private void sendTeamsMessageCard(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws JsonProcessingException, URISyntaxException {
TeamsMessageCard teamsMessageCard = new TeamsMessageCard();
teamsMessageCard.setThemeColor(Strings.emptyToNull(processedTemplate.getThemeColor()));
if (StringUtils.isEmpty(processedTemplate.getSubject())) {
teamsMessageCard.setText(processedTemplate.getBody());
} else {
teamsMessageCard.setSummary(processedTemplate.getSubject());
TeamsMessageCard.Section section = new TeamsMessageCard.Section();
section.setActivityTitle(processedTemplate.getSubject());
section.setActivitySubtitle(processedTemplate.getBody());
message.setSections(List.of(section));
teamsMessageCard.setSections(List.of(section));
}
var button = processedTemplate.getButton();
String uri = getButtonUri(processedTemplate, ctx);
if (StringUtils.isNotBlank(uri) && button.getText() != null) {
TeamsMessageCard.ActionCard actionCard = new TeamsMessageCard.ActionCard();
actionCard.setType("OpenUri");
actionCard.setName(button.getText());
var target = new TeamsMessageCard.ActionCard.Target("default", uri);
actionCard.setTargets(List.of(target));
teamsMessageCard.setPotentialAction(List.of(actionCard));
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity<>(JacksonUtil.toString(teamsMessageCard), headers);
restTemplate.postForEntity(new URI(targetConfig.getWebhookUrl()), request, String.class);
}
private String getButtonUri(MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws JsonProcessingException {
var button = processedTemplate.getButton();
if (button != null && button.isEnabled()) {
String uri;
@ -99,17 +178,9 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
} else {
uri = button.getLink();
}
if (StringUtils.isNotBlank(uri) && button.getText() != null) {
Message.ActionCard actionCard = new Message.ActionCard();
actionCard.setType("OpenUri");
actionCard.setName(button.getText());
var target = new Message.ActionCard.Target("default", uri);
actionCard.setTargets(List.of(target));
message.setPotentialAction(List.of(actionCard));
}
return uri;
}
restTemplate.postForEntity(targetConfig.getWebhookUrl(), message, String.class);
return null;
}
@Override
@ -121,74 +192,4 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
return NotificationDeliveryMethod.MICROSOFT_TEAMS;
}
@Data
public static class Message {
@JsonProperty("@type")
private final String type = "MessageCard";
@JsonProperty("@context")
private final String context = "http://schema.org/extensions";
private String themeColor;
private String summary;
private String text;
private List<Section> sections;
private List<ActionCard> potentialAction;
@Data
public static class Section {
private String activityTitle;
private String activitySubtitle;
private String activityImage;
private List<Fact> facts;
private boolean markdown;
@Data
public static class Fact {
private final String name;
private final String value;
}
}
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class ActionCard {
@JsonProperty("@type")
private String type; // ActionCard, OpenUri
private String name;
private List<Input> inputs; // for ActionCard
private List<Action> actions; // for ActionCard
private List<Target> targets;
@Data
public static class Input {
@JsonProperty("@type")
private String type; // TextInput, DateInput, MultichoiceInput
private String id;
private boolean isMultiple;
private String title;
private boolean isMultiSelect;
@Data
public static class Choice {
private final String display;
private final String value;
}
}
@Data
public static class Action {
@JsonProperty("@type")
private final String type; // HttpPOST
private final String name;
private final String target; // url
}
@Data
public static class Target {
private final String os;
private final String uri;
}
}
}
}

93
application/src/main/java/org/thingsboard/server/service/notification/channels/TeamsAdaptiveCard.java

@ -0,0 +1,93 @@
/**
* 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.service.notification.channels;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.dao.util.ImageUtils;
import java.util.ArrayList;
import java.util.List;
/**
* @link <a href="https://adaptivecards.io/designer/">AdaptiveCard Designer</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TeamsAdaptiveCard {
private String type = "message";
private List<Attachment> attachments;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Attachment {
private String contentType = "application/vnd.microsoft.card.adaptive";
private AdaptiveCard content;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class AdaptiveCard {
@JsonProperty("$schema")
private final String schema = "http://adaptivecards.io/schemas/adaptive-card.json";
private final String type = "AdaptiveCard";
private BackgroundImage backgroundImage;
@JsonProperty("body")
private List<TextBlock> textBlocks = new ArrayList<>();
private List<ActionOpenUrl> actions = new ArrayList<>();
}
@Data
@NoArgsConstructor
public static class BackgroundImage {
private String url;
private final String fillMode = "repeat";
public BackgroundImage(String color) {
// This is the only one way how to specify color the custom color for the card
url = ImageUtils.getEmbeddedBase64EncodedImg(color);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class TextBlock {
private final String type = "TextBlock";
private String text;
private String weight = "Normal";
private String size = "Medium";
private String spacing = "None";
private String color = "#FFFFFF";
private final boolean wrap = true;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class ActionOpenUrl {
private final String type = "Action.OpenUrl";
private String title;
private String url;
}
}

92
application/src/main/java/org/thingsboard/server/service/notification/channels/TeamsMessageCard.java

@ -0,0 +1,92 @@
/**
* 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.service.notification.channels;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
public class TeamsMessageCard {
@JsonProperty("@type")
private final String type = "MessageCard";
@JsonProperty("@context")
private final String context = "http://schema.org/extensions";
private String themeColor;
private String summary;
private String text;
private List<Section> sections;
private List<ActionCard> potentialAction;
@Data
public static class Section {
private String activityTitle;
private String activitySubtitle;
private String activityImage;
private List<Fact> facts;
private boolean markdown;
@Data
public static class Fact {
private final String name;
private final String value;
}
}
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class ActionCard {
@JsonProperty("@type")
private String type; // ActionCard, OpenUri
private String name;
private List<Input> inputs; // for ActionCard
private List<Action> actions; // for ActionCard
private List<Target> targets;
@Data
public static class Input {
@JsonProperty("@type")
private String type; // TextInput, DateInput, MultichoiceInput
private String id;
private boolean isMultiple;
private String title;
private boolean isMultiSelect;
@Data
public static class Choice {
private final String display;
private final String value;
}
}
@Data
public static class Action {
@JsonProperty("@type")
private final String type; // HttpPOST
private final String name;
private final String target; // url
}
@Data
public static class Target {
private final String os;
private final String uri;
}
}
}

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

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

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

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

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

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

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

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

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

@ -85,7 +85,7 @@ import java.util.stream.Collectors;
@Service
public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionService {
private final ConcurrentMap<String, Map<Integer, TbSubscription<?>>> subscriptionsBySessionId = new ConcurrentHashMap<>();
private final ConcurrentMap<String, ConcurrentMap<Integer, TbSubscription<?>>> subscriptionsBySessionId = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, TbEntityLocalSubsInfo> subscriptionsByEntityId = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, TbEntityUpdatesInfo> entityUpdates = new ConcurrentHashMap<>();
@ -101,9 +101,9 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
private ExecutorService tsCallBackExecutor;
private ScheduledExecutorService staleSessionCleanupExecutor;
@Value("${server.ws.rate_limits.subscriptions_per_tenant:2000:60}")
@Value("${server.ws.rate_limits.subscriptions_per_tenant:}")
private String subscriptionsPerTenantRateLimit;
@Value("${server.ws.rate_limits.subscriptions_per_user:500:60}")
@Value("${server.ws.rate_limits.subscriptions_per_user:}")
private String subscriptionsPerUserRateLimit;
public DefaultTbLocalSubscriptionService(AttributesService attrService, TimeseriesService tsService, TbServiceInfoProvider serviceInfoProvider,

5
application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java

@ -417,7 +417,8 @@ public class DefaultTransportApiService implements TransportApiService {
requestMsg.getCredentialsDataProto().getValidateDeviceX509CertRequestMsg().getHash()),
new ProvisionDeviceProfileCredentials(
requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceKey(),
requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret())));
requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret()),
requestMsg.getGateway()));
} catch (ProvisionFailedException e) {
return getTransportApiResponseMsg(new DeviceCredentials(), TransportProtos.ResponseStatus.valueOf(e.getMessage()));
}
@ -665,7 +666,7 @@ public class DefaultTransportApiService implements TransportApiService {
private ProvisionRequest createProvisionRequest(String certificateValue) {
return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE,
new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue),
null);
null, null);
}
}

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

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

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

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

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

@ -84,9 +84,9 @@ server:
auth_timeout_ms: "${TB_SERVER_WS_AUTH_TIMEOUT_MS:10000}"
rate_limits:
# Per-tenant rate limit for WS subscriptions
subscriptions_per_tenant: "${TB_SERVER_WS_SUBSCRIPTIONS_PER_TENANT_RATE_LIMIT:2000:60}"
subscriptions_per_tenant: "${TB_SERVER_WS_SUBSCRIPTIONS_PER_TENANT_RATE_LIMIT:}"
# Per-user rate limit for WS subscriptions
subscriptions_per_user: "${TB_SERVER_WS_SUBSCRIPTIONS_PER_USER_RATE_LIMIT:500:60}"
subscriptions_per_user: "${TB_SERVER_WS_SUBSCRIPTIONS_PER_USER_RATE_LIMIT:}"
rest:
server_side_rpc:
# Minimum value of the server-side RPC timeout. May override value provided in the REST API call.
@ -1658,6 +1658,8 @@ queue:
connection_timeout: "${TB_QUEUE_RABBIT_MQ_CONNECTION_TIMEOUT:60000}"
# RabbitMQ has a timeout for connection handshake. When clients run in heavily constrained environments, it may be necessary to increase the timeout
handshake_timeout: "${TB_QUEUE_RABBIT_MQ_HANDSHAKE_TIMEOUT:10000}"
# The maximum number of messages returned in a single call of doPoll() method
max_poll_messages: "${TB_QUEUE_RABBIT_MQ_MAX_POLL_MESSAGES:1}"
queue-properties:
# RabbitMQ properties for Rule Engine queues
rule-engine: "${TB_QUEUE_RABBIT_MQ_RE_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}"

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

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

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

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

4
application/src/test/java/org/thingsboard/server/controller/DomainControllerTest.java

@ -107,8 +107,8 @@ public class DomainControllerTest extends AbstractControllerTest {
doPut("/api/domain/" + savedDomain.getId() + "/oauth2Clients", List.of(savedOAuth2Client.getId().getId(), savedOAuth2Client2.getId().getId()));
DomainInfo retrievedDomainInfo = doGet("/api/domain/info/{id}", DomainInfo.class, savedDomain.getId().getId());
assertThat(retrievedDomainInfo).isEqualTo(new DomainInfo(savedDomain, List.of(new OAuth2ClientInfo(savedOAuth2Client),
new OAuth2ClientInfo(savedOAuth2Client2))));
assertThat(retrievedDomainInfo).isEqualTo(new DomainInfo(savedDomain, List.of(new OAuth2ClientInfo(savedOAuth2Client2),
new OAuth2ClientInfo(savedOAuth2Client))));
doPut("/api/domain/" + savedDomain.getId() + "/oauth2Clients", List.of(savedOAuth2Client2.getId().getId()));
DomainInfo retrievedDomainInfo2 = doGet("/api/domain/info/{id}", DomainInfo.class, savedDomain.getId().getId());

4
application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java

@ -109,8 +109,8 @@ public class MobileAppControllerTest extends AbstractControllerTest {
doPut("/api/mobileApp/" + savedMobileApp.getId() + "/oauth2Clients", List.of(savedOAuth2Client.getId().getId(), savedOAuth2Client2.getId().getId()));
MobileAppInfo retrievedMobileAppInfo = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId());
assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppInfo(savedMobileApp, List.of(new OAuth2ClientInfo(savedOAuth2Client),
new OAuth2ClientInfo(savedOAuth2Client2))));
assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppInfo(savedMobileApp, List.of(new OAuth2ClientInfo(savedOAuth2Client2),
new OAuth2ClientInfo(savedOAuth2Client))));
doPut("/api/mobileApp/" + savedMobileApp.getId() + "/oauth2Clients", List.of(savedOAuth2Client2.getId().getId()));
MobileAppInfo retrievedMobileAppInfo2 = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId());

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

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

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

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

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

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

2
application/src/test/java/org/thingsboard/server/service/device/provision/DeviceProvisionServiceTest.java

@ -236,7 +236,7 @@ public class DeviceProvisionServiceTest {
private ProvisionRequest createProvisionRequest(String certificateValue) {
return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE,
new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue),
null);
null, null);
}
public static String certTrimNewLinesForChainInDeviceProfile(String input) {

86
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -25,6 +25,7 @@ import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpEntity;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.JacksonUtil;
@ -85,8 +86,12 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.notification.channels.MicrosoftTeamsNotificationChannel;
import org.thingsboard.server.service.notification.channels.TeamsAdaptiveCard;
import org.thingsboard.server.service.notification.channels.TeamsMessageCard;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -752,7 +757,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
}
@Test
public void testMicrosoftTeamsNotifications() throws Exception {
public void testMicrosoftTeamsNotificationsWithOfficeConnector() throws URISyntaxException {
RestTemplate restTemplate = mock(RestTemplate.class);
microsoftTeamsNotificationChannel.setRestTemplate(restTemplate);
@ -760,6 +765,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
var targetConfig = new MicrosoftTeamsNotificationTargetConfig();
targetConfig.setWebhookUrl(webhookUrl);
targetConfig.setChannelName("My channel");
targetConfig.setUseOldApi(true);
NotificationTarget target = new NotificationTarget();
target.setName("Microsoft Teams channel");
target.setConfiguration(targetConfig);
@ -770,7 +776,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
String templateParams = "${recipientTitle} - ${entityType}";
template.setSubject("Subject: " + templateParams);
template.setBody("Body: " + templateParams);
template.setThemeColor("ff0000");
template.setThemeColor("#ff0000");
var button = new MicrosoftTeamsDeliveryMethodNotificationTemplate.Button();
button.setEnabled(true);
button.setText("Button: " + templateParams);
@ -803,11 +809,13 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
assertThat(preview.getRecipientsCountByTarget().get(target.getName())).isEqualTo(1);
assertThat(preview.getRecipientsPreview()).containsOnly(targetConfig.getChannelName());
var messageCaptor = ArgumentCaptor.forClass(MicrosoftTeamsNotificationChannel.Message.class);
ArgumentCaptor<HttpEntity<String>> messageCaptor = ArgumentCaptor.forClass(HttpEntity.class);
notificationCenter.processNotificationRequest(tenantId, notificationRequest, null);
verify(restTemplate, timeout(20000)).postForEntity(eq(webhookUrl), messageCaptor.capture(), any());
verify(restTemplate, timeout(20000)).postForEntity(eq(new URI(webhookUrl)), messageCaptor.capture(), any());
HttpEntity<String> value = messageCaptor.getValue();
TeamsMessageCard message = JacksonUtil.fromString(value.getBody(), TeamsMessageCard.class);
var message = messageCaptor.getValue();
String expectedParams = "My channel - Device";
assertThat(message.getThemeColor()).isEqualTo(template.getThemeColor());
assertThat(message.getSections().get(0).getActivityTitle()).isEqualTo("Subject: " + expectedParams);
@ -816,6 +824,74 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
assertThat(message.getPotentialAction().get(0).getTargets().get(0).getUri()).isEqualTo("https://" + expectedParams);
}
@Test
public void testMicrosoftTeamsNotificationsWithWorkflow() throws Exception {
RestTemplate restTemplate = mock(RestTemplate.class);
microsoftTeamsNotificationChannel.setRestTemplate(restTemplate);
String webhookUrl = "https://webhook.com/webhookb2/9628fa60-d873-11ed-913c-a196b1f9b445";
var targetConfig = new MicrosoftTeamsNotificationTargetConfig();
targetConfig.setWebhookUrl(webhookUrl);
targetConfig.setChannelName("My channel");
targetConfig.setUseOldApi(false);
NotificationTarget target = new NotificationTarget();
target.setName("Microsoft Teams channel");
target.setConfiguration(targetConfig);
target = saveNotificationTarget(target);
var template = new MicrosoftTeamsDeliveryMethodNotificationTemplate();
template.setEnabled(true);
String templateParams = "${recipientTitle} - ${entityType}";
template.setSubject("Subject: " + templateParams);
template.setBody("Body: " + templateParams);
template.setThemeColor("#ff0000");
var button = new MicrosoftTeamsDeliveryMethodNotificationTemplate.Button();
button.setEnabled(true);
button.setText("Button: " + templateParams);
button.setLinkType(LinkType.LINK);
button.setLink("https://" + templateParams);
template.setButton(button);
NotificationTemplate notificationTemplate = new NotificationTemplate();
notificationTemplate.setName("Notification to Teams");
notificationTemplate.setNotificationType(NotificationType.GENERAL);
NotificationTemplateConfig templateConfig = new NotificationTemplateConfig();
templateConfig.setDeliveryMethodsTemplates(Map.of(
NotificationDeliveryMethod.MICROSOFT_TEAMS, template
));
notificationTemplate.setConfiguration(templateConfig);
notificationTemplate = saveNotificationTemplate(notificationTemplate);
NotificationRequest notificationRequest = NotificationRequest.builder()
.tenantId(tenantId)
.originatorEntityId(tenantAdminUserId)
.templateId(notificationTemplate.getId())
.targets(List.of(target.getUuidId()))
.info(EntityActionNotificationInfo.builder()
.entityId(new DeviceId(UUID.randomUUID()))
.actionType(ActionType.ADDED)
.userId(tenantAdminUserId.getId())
.build())
.build();
NotificationRequestPreview preview = doPost("/api/notification/request/preview", notificationRequest, NotificationRequestPreview.class);
assertThat(preview.getRecipientsCountByTarget().get(target.getName())).isEqualTo(1);
assertThat(preview.getRecipientsPreview()).containsOnly(targetConfig.getChannelName());
ArgumentCaptor<HttpEntity<String>> messageCaptor = ArgumentCaptor.forClass(HttpEntity.class);
notificationCenter.processNotificationRequest(tenantId, notificationRequest, null);
verify(restTemplate, timeout(20000)).postForEntity(eq(new URI(webhookUrl)), messageCaptor.capture(), any());
HttpEntity<String> value = messageCaptor.getValue();
TeamsAdaptiveCard message = JacksonUtil.fromString(value.getBody(), TeamsAdaptiveCard.class);
String expectedParams = "My channel - Device";
assertThat(message).isNotNull();
assertThat(message.getAttachments().get(0).getContent().getBackgroundImage().getUrl()).isNotEmpty();
assertThat(message.getAttachments().get(0).getContent().getTextBlocks().get(0).getText()).isEqualTo("Subject: " + expectedParams);
assertThat(message.getAttachments().get(0).getContent().getTextBlocks().get(1).getText()).isEqualTo("Body: " + expectedParams);
assertThat(message.getAttachments().get(0).getContent().getActions().get(0).getTitle()).isEqualTo("Button: " + expectedParams);
assertThat(message.getAttachments().get(0).getContent().getActions().get(0).getUrl()).isEqualTo("https://" + expectedParams);
}
@Test
public void testMobileAppNotifications() throws Exception {
loginCustomerUser();

59
application/src/test/java/org/thingsboard/server/transport/coap/provision/CoapProvisionJsonDeviceTest.java

@ -23,6 +23,7 @@ import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.CoapDeviceType;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -67,6 +68,11 @@ public class CoapProvisionJsonDeviceTest extends AbstractCoapIntegrationTest {
processTestProvisioningCreateNewDeviceWithoutCredentials();
}
@Test
public void testProvisioningCreateNewGatewayDevice() throws Exception {
processTestProvisioningCreateNewGatewayDevice();
}
@Test
public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception {
processTestProvisioningCreateNewDeviceWithAccessToken();
@ -123,6 +129,37 @@ public class CoapProvisionJsonDeviceTest extends AbstractCoapIntegrationTest {
}
private void processTestProvisioningCreateNewGatewayDevice() throws Exception {
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()
.deviceName("Test Provision device3")
.coapDeviceType(CoapDeviceType.DEFAULT)
.transportPayloadType(TransportPayloadType.JSON)
.provisionType(DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES)
.provisionKey("testProvisionKey")
.provisionSecret("testProvisionSecret")
.build();
processBeforeTest(configProperties);
JsonNode response = JacksonUtil.fromBytes(createCoapClientAndPublish(true));
Assert.assertTrue(response.hasNonNull("credentialsType"));
Assert.assertTrue(response.hasNonNull("status"));
Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device");
Assert.assertNotNull(createdDevice);
JsonNode additionalInfo = createdDevice.getAdditionalInfo();
Assert.assertNotNull(additionalInfo);
Assert.assertTrue(additionalInfo.has(DataConstants.GATEWAY_PARAMETER)
&& additionalInfo.get(DataConstants.GATEWAY_PARAMETER).isBoolean());
Assert.assertTrue(additionalInfo.get(DataConstants.GATEWAY_PARAMETER).asBoolean());
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").asText());
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("status").asText());
}
private void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception {
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()
.deviceName("Test Provision device3")
@ -222,16 +259,30 @@ public class CoapProvisionJsonDeviceTest extends AbstractCoapIntegrationTest {
}
private byte[] createCoapClientAndPublish() throws Exception {
return createCoapClientAndPublish("");
return createCoapClientAndPublish(false);
}
private byte[] createCoapClientAndPublish(boolean isGateway) throws Exception {
return createCoapClientAndPublish("", isGateway);
}
private byte[] createCoapClientAndPublish(String deviceCredentials) throws Exception {
String provisionRequestMsg = createTestProvisionMessage(deviceCredentials);
return createCoapClientAndPublish(deviceCredentials, false);
}
private byte[] createCoapClientAndPublish(String deviceCredentials, boolean isGateway) throws Exception {
String provisionRequestMsg = createTestProvisionMessage(deviceCredentials, isGateway);
client = new CoapTestClient(accessToken, FeatureType.PROVISION);
return client.postMethod(provisionRequestMsg.getBytes()).getPayload();
}
private String createTestProvisionMessage(String deviceCredentials) {
return "{\"deviceName\":\"Test Provision device\",\"provisionDeviceKey\":\"testProvisionKey\", \"provisionDeviceSecret\":\"testProvisionSecret\"" + deviceCredentials + "}";
protected String createTestProvisionMessage(String deviceCredentials, boolean isGateway) {
String request = "{\"deviceName\":\"Test Provision device\",\"provisionDeviceKey\":\"testProvisionKey\", \"provisionDeviceSecret\":\"testProvisionSecret\""
+ deviceCredentials;
if (isGateway) {
request += ",\"gateway\":true";
}
request += "}";
return request;
}
}

44
application/src/test/java/org/thingsboard/server/transport/coap/provision/CoapProvisionProtoDeviceTest.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.transport.coap.provision;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResponse;
import org.junit.After;
@ -22,6 +23,7 @@ import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.CoapDeviceType;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -74,6 +76,11 @@ public class CoapProvisionProtoDeviceTest extends AbstractCoapIntegrationTest {
processTestProvisioningCreateNewDeviceWithoutCredentials();
}
@Test
public void testProvisioningCreateNewGatewayDevice() throws Exception {
processTestProvisioningCreateNewGatewayDevice();
}
@Test
public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception {
processTestProvisioningCreateNewDeviceWithAccessToken();
@ -124,6 +131,35 @@ public class CoapProvisionProtoDeviceTest extends AbstractCoapIntegrationTest {
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getStatus().name());
}
private void processTestProvisioningCreateNewGatewayDevice() throws Exception {
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()
.deviceName("Test Provision device3")
.coapDeviceType(CoapDeviceType.DEFAULT)
.transportPayloadType(TransportPayloadType.PROTOBUF)
.provisionType(DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES)
.provisionKey("testProvisionKey")
.provisionSecret("testProvisionSecret")
.build();
processBeforeTest(configProperties);
byte[] testsProvisionMessage = createTestsProvisionMessage(null, null, true);
ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createCoapClientAndPublish(testsProvisionMessage));
Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device");
Assert.assertNotNull(createdDevice);
JsonNode additionalInfo = createdDevice.getAdditionalInfo();
Assert.assertNotNull(additionalInfo);
Assert.assertTrue(additionalInfo.has(DataConstants.GATEWAY_PARAMETER)
&& additionalInfo.get(DataConstants.GATEWAY_PARAMETER).isBoolean());
Assert.assertTrue(additionalInfo.get(DataConstants.GATEWAY_PARAMETER).asBoolean());
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getCredentialsType().name());
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getStatus().name());
}
private void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception {
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()
.deviceName("Test Provision device3")
@ -233,6 +269,10 @@ public class CoapProvisionProtoDeviceTest extends AbstractCoapIntegrationTest {
}
private byte[] createTestsProvisionMessage(CredentialsType credentialsType, CredentialsDataProto credentialsData) throws Exception {
return createTestsProvisionMessage(credentialsType, credentialsData, false);
}
private byte[] createTestsProvisionMessage(CredentialsType credentialsType, CredentialsDataProto credentialsData, boolean isGateway) throws Exception {
return ProvisionDeviceRequestMsg.newBuilder()
.setDeviceName("Test Provision device")
.setCredentialsType(credentialsType != null ? credentialsType : CredentialsType.ACCESS_TOKEN)
@ -241,7 +281,9 @@ public class CoapProvisionProtoDeviceTest extends AbstractCoapIntegrationTest {
ProvisionDeviceCredentialsMsg.newBuilder()
.setProvisionDeviceKey("testProvisionKey")
.setProvisionDeviceSecret("testProvisionSecret")
).build()
)
.setGateway(isGateway)
.build()
.toByteArray();
}

58
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionJsonDeviceTest.java

@ -22,6 +22,7 @@ import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -68,6 +69,11 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest {
processTestProvisioningCreateNewDeviceWithoutCredentials();
}
@Test
public void testProvisioningCreateNewGatewayDevice() throws Exception {
processTestProvisioningCreateNewGatewayDevice();
}
@Test
public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception {
processTestProvisioningCreateNewDeviceWithAccessToken();
@ -130,6 +136,37 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest {
}
protected void processTestProvisioningCreateNewGatewayDevice() throws Exception {
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.deviceName("Test Provision device3")
.transportPayloadType(TransportPayloadType.JSON)
.provisionType(DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES)
.provisionKey("testProvisionKey")
.provisionSecret("testProvisionSecret")
.build();
super.processBeforeTest(configProperties);
byte[] result = createMqttClientAndPublish(true);
JsonNode response = JacksonUtil.fromBytes(result);
Assert.assertTrue(response.hasNonNull("credentialsType"));
Assert.assertTrue(response.hasNonNull("status"));
Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device");
Assert.assertNotNull(createdDevice);
JsonNode additionalInfo = createdDevice.getAdditionalInfo();
Assert.assertNotNull(additionalInfo);
Assert.assertTrue(additionalInfo.has(DataConstants.GATEWAY_PARAMETER)
&& additionalInfo.get(DataConstants.GATEWAY_PARAMETER).isBoolean());
Assert.assertTrue(additionalInfo.get(DataConstants.GATEWAY_PARAMETER).asBoolean());
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").asText());
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("status").asText());
}
protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception {
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.deviceName("Test Provision device3")
@ -264,11 +301,19 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest {
}
protected byte[] createMqttClientAndPublish() throws Exception {
return createMqttClientAndPublish("");
return createMqttClientAndPublish(false);
}
protected byte[] createMqttClientAndPublish(boolean isGateway) throws Exception {
return createMqttClientAndPublish("", isGateway);
}
protected byte[] createMqttClientAndPublish(String deviceCredentials) throws Exception {
String provisionRequestMsg = createTestProvisionMessage(deviceCredentials);
return createMqttClientAndPublish(deviceCredentials, false);
}
protected byte[] createMqttClientAndPublish(String deviceCredentials, boolean isGateway) throws Exception {
String provisionRequestMsg = createTestProvisionMessage(deviceCredentials, isGateway);
MqttTestClient client = new MqttTestClient();
client.connectAndWait("provision");
MqttTestCallback onProvisionCallback = new MqttTestSubscribeOnTopicCallback(DEVICE_PROVISION_RESPONSE_TOPIC);
@ -280,7 +325,12 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest {
return onProvisionCallback.getPayloadBytes();
}
protected String createTestProvisionMessage(String deviceCredentials) {
return "{\"deviceName\":\"Test Provision device\",\"provisionDeviceKey\":\"testProvisionKey\", \"provisionDeviceSecret\":\"testProvisionSecret\"" + deviceCredentials + "}";
protected String createTestProvisionMessage(String deviceCredentials, boolean isGateway) {
String request = "{\"deviceName\":\"Test Provision device\",\"provisionDeviceKey\":\"testProvisionKey\", \"provisionDeviceSecret\":\"testProvisionSecret\"" + deviceCredentials;
if (isGateway) {
request += ",\"gateway\":true";
}
request += "}";
return request;
}
}

45
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionProtoDeviceTest.java

@ -15,12 +15,14 @@
*/
package org.thingsboard.server.transport.mqtt.mqttv3.provision;
import com.fasterxml.jackson.databind.JsonNode;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -76,6 +78,11 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest {
processTestProvisioningCreateNewDeviceWithoutCredentials();
}
@Test
public void testProvisioningCreateNewGatewayDevice() throws Exception {
processTestProvisioningCreateNewGatewayDevice();
}
@Test
public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception {
processTestProvisioningCreateNewDeviceWithAccessToken();
@ -130,6 +137,36 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest {
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getStatus().name());
}
protected void processTestProvisioningCreateNewGatewayDevice() throws Exception {
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.deviceName("Test Provision device3")
.transportPayloadType(TransportPayloadType.PROTOBUF)
.provisionType(DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES)
.provisionKey("testProvisionKey")
.provisionSecret("testProvisionSecret")
.build();
processBeforeTest(configProperties);
byte[] provisionRequestMsg = createTestsProvisionMessage(null, null, true);
byte[] responseBytesMsg = createMqttClientAndPublish(provisionRequestMsg);
ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(responseBytesMsg);
Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device");
Assert.assertNotNull(createdDevice);
JsonNode additionalInfo = createdDevice.getAdditionalInfo();
Assert.assertNotNull(additionalInfo);
Assert.assertTrue(additionalInfo.has(DataConstants.GATEWAY_PARAMETER)
&& additionalInfo.get(DataConstants.GATEWAY_PARAMETER).isBoolean());
Assert.assertTrue(additionalInfo.get(DataConstants.GATEWAY_PARAMETER).asBoolean());
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getCredentialsType().name());
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getStatus().name());
}
protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception {
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.deviceName("Test Provision device3")
@ -280,6 +317,10 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest {
}
protected byte[] createTestsProvisionMessage(CredentialsType credentialsType, CredentialsDataProto credentialsData) throws Exception {
return createTestsProvisionMessage(credentialsType, credentialsData, false);
}
protected byte[] createTestsProvisionMessage(CredentialsType credentialsType, CredentialsDataProto credentialsData, boolean isGateway) throws Exception {
return ProvisionDeviceRequestMsg.newBuilder()
.setDeviceName("Test Provision device")
.setCredentialsType(credentialsType != null ? credentialsType : CredentialsType.ACCESS_TOKEN)
@ -288,7 +329,9 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest {
ProvisionDeviceCredentialsMsg.newBuilder()
.setProvisionDeviceKey("testProvisionKey")
.setProvisionDeviceSecret("testProvisionSecret")
).build()
)
.setGateway(isGateway)
.build()
.toByteArray();
}

148
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/MqttClientSideRpcIntegrationTest.java

@ -0,0 +1,148 @@
/**
* 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.transport.mqtt.mqttv3.rpc;
import io.netty.handler.codec.mqtt.MqttQoS;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.TransportPayloadType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest;
import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties;
import org.thingsboard.server.transport.mqtt.limits.GatewaySessionLimits;
import org.thingsboard.server.transport.mqtt.limits.SessionLimits;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestSubscribeOnTopicCallback;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_TOPIC;
import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_RESPONSE_SUB_TOPIC;
import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_RESPONSE_TOPIC;
@DaoSqlTest
public class MqttClientSideRpcIntegrationTest extends AbstractMqttIntegrationTest {
@Value("${transport.mqtt.netty.max_payload_size}")
private Integer maxPayloadSize;
@Value("${transport.mqtt.msg_queue_size_per_device_limit}")
private int maxInflightMessages;
@Test
public void getSessionLimitsRpcForDeviceTest() throws Exception {
loginSysAdmin();
TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId, TenantProfile.class);
DefaultTenantProfileConfiguration profileConfiguration = tenantProfile.getDefaultProfileConfiguration();
profileConfiguration.setTransportDeviceMsgRateLimit("20:600");
profileConfiguration.setTransportDeviceTelemetryMsgRateLimit("10:600");
profileConfiguration.setTransportDeviceTelemetryDataPointsRateLimit("40:600");
doPost("/api/tenantProfile", tenantProfile);
var expectedLimits = new SessionLimits();
var deviceLimits = new SessionLimits.SessionRateLimits(profileConfiguration.getTransportDeviceMsgRateLimit(),
profileConfiguration.getTransportDeviceTelemetryMsgRateLimit(),
profileConfiguration.getTransportDeviceTelemetryDataPointsRateLimit());
expectedLimits.setRateLimits(deviceLimits);
expectedLimits.setMaxPayloadSize(maxPayloadSize);
expectedLimits.setMaxInflightMessages(maxInflightMessages);
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.deviceName("Test Get Service Configuration")
.transportPayloadType(TransportPayloadType.JSON)
.build();
processBeforeTest(configProperties);
MqttTestClient client = new MqttTestClient();
client.connectAndWait(accessToken);
MqttTestCallback callback = new MqttTestSubscribeOnTopicCallback(DEVICE_RPC_RESPONSE_TOPIC + "1");
client.setCallback(callback);
client.subscribeAndWait(DEVICE_RPC_RESPONSE_SUB_TOPIC, MqttQoS.AT_MOST_ONCE);
client.publishAndWait(DEVICE_RPC_REQUESTS_TOPIC + "1", "{\"method\":\"getSessionLimits\",\"params\":{}}".getBytes());
assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await callback").isTrue();
var payload = callback.getPayloadBytes();
SessionLimits actualLimits = JacksonUtil.fromBytes(payload, SessionLimits.class);
assertEquals(expectedLimits, actualLimits);
client.disconnect();
}
@Test
public void getSessionLimitsRpcForGatewayTest() throws Exception {
loginSysAdmin();
TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId, TenantProfile.class);
DefaultTenantProfileConfiguration profileConfiguration = tenantProfile.getDefaultProfileConfiguration();
profileConfiguration.setTransportGatewayMsgRateLimit("100:600");
profileConfiguration.setTransportGatewayTelemetryMsgRateLimit("50:600");
profileConfiguration.setTransportGatewayTelemetryDataPointsRateLimit("200:600");
profileConfiguration.setTransportGatewayDeviceMsgRateLimit("20:600");
profileConfiguration.setTransportGatewayDeviceTelemetryMsgRateLimit("10:600");
profileConfiguration.setTransportGatewayDeviceTelemetryDataPointsRateLimit("40:600");
doPost("/api/tenantProfile", tenantProfile);
var expectedLimits = new GatewaySessionLimits();
var gatewayLimits = new SessionLimits.SessionRateLimits(profileConfiguration.getTransportGatewayMsgRateLimit(),
profileConfiguration.getTransportGatewayTelemetryMsgRateLimit(),
profileConfiguration.getTransportGatewayTelemetryDataPointsRateLimit());
var gatewayDeviceLimits = new SessionLimits.SessionRateLimits(profileConfiguration.getTransportGatewayDeviceMsgRateLimit(),
profileConfiguration.getTransportGatewayDeviceTelemetryMsgRateLimit(),
profileConfiguration.getTransportGatewayDeviceTelemetryDataPointsRateLimit());
expectedLimits.setGatewayRateLimits(gatewayLimits);
expectedLimits.setRateLimits(gatewayDeviceLimits);
expectedLimits.setMaxPayloadSize(maxPayloadSize);
expectedLimits.setMaxInflightMessages(maxInflightMessages);
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.gatewayName("Test Get Service Configuration Gateway")
.transportPayloadType(TransportPayloadType.JSON)
.build();
processBeforeTest(configProperties);
MqttTestClient client = new MqttTestClient();
client.connectAndWait(gatewayAccessToken);
MqttTestCallback callback = new MqttTestSubscribeOnTopicCallback(DEVICE_RPC_RESPONSE_TOPIC + "1");
client.setCallback(callback);
client.subscribeAndWait(DEVICE_RPC_RESPONSE_SUB_TOPIC, MqttQoS.AT_MOST_ONCE);
client.publishAndWait(DEVICE_RPC_REQUESTS_TOPIC + "1", "{\"method\":\"getSessionLimits\",\"params\":{}}".getBytes());
assertTrue(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS));
var payload = callback.getPayloadBytes();
SessionLimits actualLimits = JacksonUtil.fromBytes(payload, GatewaySessionLimits.class);
assertEquals(expectedLimits, actualLimits);
client.disconnect();
}
}

2
common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java

@ -240,7 +240,7 @@ public final class TbActorMailbox implements TbActorCtx {
highPriorityMsgs.forEach(msg -> msg.onTbActorStopped(stopReason));
normalPriorityMsgs.forEach(msg -> msg.onTbActorStopped(stopReason));
} catch (Throwable t) {
log.warn("[{}] Failed to destroy actor: {}", selfId, t);
log.warn("[{}] Failed to destroy actor: ", selfId, t);
}
});
}

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

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

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

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

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

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

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

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

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

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.cache;
import java.io.Serializable;
public interface VersionedCacheKey extends Serializable {
default boolean isVersioned() {
return false;
}
}

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

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

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

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

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

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

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

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

1
common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionRequest.java

@ -28,4 +28,5 @@ public class ProvisionRequest {
private DeviceCredentialsType credentialsType;
private ProvisionDeviceCredentialsData credentialsData;
private ProvisionDeviceProfileCredentials credentials;
private Boolean gateway;
}

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

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

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

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

1
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java

@ -28,6 +28,7 @@ public class MicrosoftTeamsNotificationTargetConfig extends NotificationTargetCo
private String webhookUrl;
@NotEmpty
private String channelName;
private Boolean useOldApi = Boolean.TRUE;
@Override
public NotificationTargetType getType() {

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

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

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

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

1
common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java

@ -635,6 +635,7 @@ public class JsonConverter {
.setProvisionDeviceCredentialsMsg(buildProvisionDeviceCredentialsMsg(
getStrValue(jo, DataConstants.PROVISION_KEY, true),
getStrValue(jo, DataConstants.PROVISION_SECRET, true)))
.setGateway(jo.has(DataConstants.GATEWAY_PARAMETER) && jo.get(DataConstants.GATEWAY_PARAMETER).getAsBoolean())
.build();
}

1
common/proto/src/main/proto/queue.proto

@ -697,6 +697,7 @@ message ProvisionDeviceRequestMsg {
CredentialsType credentialsType = 2;
ProvisionDeviceCredentialsMsg provisionDeviceCredentialsMsg = 3;
CredentialsDataProto credentialsDataProto = 4;
bool gateway = 5;
}
message ProvisionDeviceCredentialsMsg {

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

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

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

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

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

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

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

@ -0,0 +1,128 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.queue.rabbitmq;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.GetResponse;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueMsgDecoder;
import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
@ExtendWith(MockitoExtension.class)
class TbRabbitMqConsumerTemplateTest {
private static final String TOPIC = "some-topic";
@Mock
private TbQueueAdmin admin;
@Mock
private ConnectionFactory connectionFactory;
@Mock
private TbQueueMsgDecoder<DefaultTbQueueMsg> decoder;
@Mock
private Connection connection;
@Mock
private Channel channel;
@Mock
private TopicPartitionInfo partition;
@Mock
private GetResponse getResponse;
private TbRabbitMqConsumerTemplate<DefaultTbQueueMsg> consumer;
private void setUpConsumerWithMaxPollMessages(int maxPollMessages) throws Exception {
when(connectionFactory.newConnection()).thenReturn(connection);
when(connection.createChannel()).thenReturn(channel);
TbRabbitMqSettings settings = new TbRabbitMqSettings();
settings.setMaxPollMessages(maxPollMessages);
settings.setConnectionFactory(connectionFactory);
consumer = new TbRabbitMqConsumerTemplate<>(admin, settings, TOPIC, decoder);
when(partition.getFullTopicName()).thenReturn(TOPIC);
consumer.subscribe(Set.of(partition));
}
@Test
void pollWithMax5PollMessagesReturnsEmptyListIfNoMessages() throws Exception {
setUpConsumerWithMaxPollMessages(5);
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(null);
assertThat(consumer.poll(0L)).isEmpty();
verify(channel).basicGet(anyString(), anyBoolean());
}
@Test
void pollWithMax5PollMessagesReturns5MessagesIfQueueContains5() throws Exception {
setUpConsumerWithMaxPollMessages(5);
when(getResponse.getBody()).thenReturn(newMessageBody());
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(getResponse);
assertThat(consumer.poll(0L)).hasSize(5);
verify(channel, times(5)).basicGet(anyString(), anyBoolean());
}
@Test
void pollWithMax1PollMessageReturns1MessageIfQueueContainsMore() throws Exception {
setUpConsumerWithMaxPollMessages(1);
when(getResponse.getBody()).thenReturn(newMessageBody());
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(getResponse);
assertThat(consumer.poll(0L)).hasSize(1);
verify(channel).basicGet(anyString(), anyBoolean());
}
@Test
void pollWithMax3PollMessagesReturns2MessagesIfQueueContains2() throws Exception {
setUpConsumerWithMaxPollMessages(3);
when(getResponse.getBody()).thenReturn(newMessageBody());
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(getResponse, getResponse, null);
assertThat(consumer.poll(0L)).hasSize(2);
verify(channel, times(3)).basicGet(anyString(), anyBoolean());
}
private byte[] newMessageBody() {
return ("{\"key\": \"" + UUID.randomUUID() + "\"}").getBytes(StandardCharsets.UTF_8);
}
}

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

@ -0,0 +1,95 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.queue.scheduler;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.assertj.core.api.Assertions.assertThat;
class DefaultSchedulerComponentTest {
DefaultSchedulerComponent schedulerComponent;
@BeforeEach
void setup() {
schedulerComponent = new DefaultSchedulerComponent();
schedulerComponent.init();
}
@AfterEach
void cleanup() {
schedulerComponent.destroy();
}
@Test
@DisplayName("scheduleAtFixedRate() should continue periodic execution even if command throws exception")
void scheduleAtFixedRateShouldNotStopPeriodicExecutionWhenCommandThrowsException() {
// GIVEN
var wasExecutedAtLeastOnce = new AtomicBoolean(false);
Runnable exceptionThrowingCommand = () -> {
try {
throw new RuntimeException("Unexpected exception");
} finally {
wasExecutedAtLeastOnce.set(true);
}
};
// WHEN
ScheduledFuture<?> future = schedulerComponent.scheduleAtFixedRate(exceptionThrowingCommand, 0, 200, TimeUnit.MILLISECONDS);
// THEN
Awaitility.await().alias("Wait until command is executed at least once")
.atMost(5, TimeUnit.SECONDS)
.until(wasExecutedAtLeastOnce::get);
assertThat(future.isDone()).as("Periodic execution should not stop after unhandled exception is thrown by the command").isFalse();
}
@Test
@DisplayName("scheduleWithFixedDelay() should continue periodic execution even if command throws exception")
void scheduleWithFixedDelayShouldNotStopPeriodicExecutionWhenCommandThrowsException() {
// GIVEN
var wasExecutedAtLeastOnce = new AtomicBoolean(false);
Runnable exceptionThrowingCommand = () -> {
try {
throw new RuntimeException("Unexpected exception");
} finally {
wasExecutedAtLeastOnce.set(true);
}
};
// WHEN
ScheduledFuture<?> future = schedulerComponent.scheduleWithFixedDelay(exceptionThrowingCommand, 0, 200, TimeUnit.MILLISECONDS);
// THEN
Awaitility.await().alias("Wait until command is executed at least once")
.atMost(5, TimeUnit.SECONDS)
.until(wasExecutedAtLeastOnce::get);
assertThat(future.isDone()).as("Periodic execution should not stop after unhandled exception is thrown by the command").isFalse();
}
}

37
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -273,13 +273,13 @@ public class TbUtils {
Long.class, boolean.class, boolean.class)));
parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex",
Long.class, boolean.class, boolean.class, int.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString",
Long.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString",
Long.class, int.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString",
Long.class, int.class, boolean.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString",
Long.class, int.class, boolean.class, boolean.class)));
parserConfig.addImport("floatToHex", new MethodStub(TbUtils.class.getMethod("floatToHex",
Float.class)));
@ -315,8 +315,6 @@ public class TbUtils {
String.class)));
parserConfig.addImport("decodeURI", new MethodStub(TbUtils.class.getMethod("decodeURI",
String.class)));
parserConfig.addImport("raiseError", new MethodStub(TbUtils.class.getMethod("raiseError",
String.class, Object.class)));
parserConfig.addImport("raiseError", new MethodStub(TbUtils.class.getMethod("raiseError",
String.class)));
parserConfig.addImport("isBinary", new MethodStub(TbUtils.class.getMethod("isBinary",
@ -327,7 +325,7 @@ public class TbUtils {
String.class)));
parserConfig.addImport("isHexadecimal", new MethodStub(TbUtils.class.getMethod("isHexadecimal",
String.class)));
parserConfig.addImport("byteArrayToExecutionArrayList", new MethodStub(TbUtils.class.getMethod("bytesToExecutionArrayList",
parserConfig.addImport("bytesToExecutionArrayList", new MethodStub(TbUtils.class.getMethod("bytesToExecutionArrayList",
ExecutionContext.class, byte[].class)));
parserConfig.addImport("padStart", new MethodStub(TbUtils.class.getMethod("padStart",
String.class, int.class, char.class)));
@ -725,19 +723,19 @@ public class TbUtils {
return prepareNumberHexString(l, bigEndian, pref, len, HEX_LEN_LONG_MAX);
}
public static String intLongToString(Long number) {
return intLongToString(number, DEC_RADIX);
public static String intLongToRadixString(Long number) {
return intLongToRadixString(number, DEC_RADIX);
}
public static String intLongToString(Long number, int radix) {
return intLongToString(number, radix, true);
public static String intLongToRadixString(Long number, int radix) {
return intLongToRadixString(number, radix, true);
}
public static String intLongToString(Long number, int radix, boolean bigEndian) {
return intLongToString(number, radix, bigEndian, false);
public static String intLongToRadixString(Long number, int radix, boolean bigEndian) {
return intLongToRadixString(number, radix, bigEndian, false);
}
public static String intLongToString(Long number, int radix, boolean bigEndian, boolean pref) {
public static String intLongToRadixString(Long number, int radix, boolean bigEndian, boolean pref) {
if (radix >= 25 && radix <= MAX_RADIX) {
return Long.toString(number, radix);
}
@ -1150,12 +1148,7 @@ public class TbUtils {
}
public static void raiseError(String message) {
raiseError(message, null);
}
public static void raiseError(String message, Object value) {
String msg = value == null ? message : message + " for value " + value;
throw new RuntimeException(msg);
throw new RuntimeException(message);
}
private static void parseRecursive(Object json, Map<String, Object> map, List<String> excludeList, String path, boolean pathInKey) {
@ -1344,8 +1337,8 @@ public class TbUtils {
* Writes the bit value to the appropriate location in the bins array, starting at the end of the array,
* to ensure proper alignment (highest bit to low end).
*/
public static byte[] parseLongToBinaryArray(long longValue, int binsLength) {
int len = Math.min(binsLength, BYTES_LEN_LONG_MAX * BIN_LEN_MAX);
public static byte[] parseLongToBinaryArray(long longValue, int binLength) {
int len = Math.min(binLength, BYTES_LEN_LONG_MAX * BIN_LEN_MAX);
byte[] bins = new byte[len];
for (int i = 0; i < len; i++) {
bins[len - 1 - i] = (byte) ((longValue >> i) & 1);

94
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java

@ -19,6 +19,7 @@ import com.google.common.collect.Lists;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.units.qual.A;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@ -330,8 +331,8 @@ public class TbUtilsTest {
@Test
public void parseBytesIntToFloat() {
byte[] intValByte = {0x00, 0x00, 0x00, 0x0A};
Float valueExpected = 10.0f;
Float valueActual = TbUtils.parseBytesIntToFloat(intValByte, 3, 1, true);
float valueExpected = 10.0f;
float valueActual = TbUtils.parseBytesIntToFloat(intValByte, 3, 1, true);
Assertions.assertEquals(valueExpected, valueActual);
valueActual = TbUtils.parseBytesIntToFloat(intValByte, 3, 1, false);
Assertions.assertEquals(valueExpected, valueActual);
@ -345,20 +346,23 @@ public class TbUtilsTest {
valueExpected = 10.0f;
valueActual = TbUtils.parseBytesIntToFloat(intValByte, 0, 4, true);
Assertions.assertEquals(valueExpected, valueActual);
valueExpected = 1.6777216E8f;
valueActual = TbUtils.parseBytesIntToFloat(intValByte, 0, 4, false);
valueExpected = 167772.16f;
double factor = 1e3;
valueActual = (float) (TbUtils.parseBytesIntToFloat(intValByte, 0, 4, false) / factor);
Assertions.assertEquals(valueExpected, valueActual);
String dataAT101 = "0x01756403671B01048836BF7701F000090722050000";
List<Byte> byteAT101 = TbUtils.hexToBytes(ctx, dataAT101);
float latitudeExpected = 24.62495f;
int offset = 9;
valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset, 4, false);
Assertions.assertEquals(latitudeExpected, valueActual / 1000000);
factor = 1e6;
valueActual = (float) (TbUtils.parseBytesIntToFloat(byteAT101, offset, 4, false) / factor);
Assertions.assertEquals(latitudeExpected, valueActual);
float longitudeExpected = 118.030576f;
valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset + 4, 4, false);
Assertions.assertEquals(longitudeExpected, valueActual / 1000000);
valueActual = (float) (TbUtils.parseBytesIntToFloat(byteAT101, offset + 4, 4, false) / factor);
Assertions.assertEquals(longitudeExpected, valueActual);
valueExpected = 9.185175E8f;
valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset);
@ -520,6 +524,18 @@ public class TbUtilsTest {
valueExpected = 7.2057594037927936E17d;
valueActual = TbUtils.parseBytesLongToDouble(longValByte, 0, 8, false);
Assertions.assertEquals(valueExpected, valueActual);
String dataPalacKiyv = "0x32D009423F23B300B0106E08D96B6C00";
List<Byte> byteAT101 = TbUtils.hexToBytes(ctx, dataPalacKiyv);
double latitudeExpected = 50.422775429058610d;
int offset = 0;
double factor = 1e15;
valueActual = TbUtils.parseBytesLongToDouble(byteAT101, offset, 8, false);
Assertions.assertEquals(latitudeExpected, valueActual / factor);
double longitudeExpected = 30.517877378257072d;
valueActual = TbUtils.parseBytesLongToDouble(byteAT101, offset + 8, 8, false);
Assertions.assertEquals(longitudeExpected, valueActual / factor);
}
@Test
@ -720,27 +736,27 @@ public class TbUtilsTest {
@Test
public void numberToString_Test() {
Assertions.assertEquals("00111010", TbUtils.intLongToString(58L, MIN_RADIX));
Assertions.assertEquals("0000000010011110", TbUtils.intLongToString(158L, MIN_RADIX));
Assertions.assertEquals("00000000000000100000001000000001", TbUtils.intLongToString(131585L, MIN_RADIX));
Assertions.assertEquals("0111111111111111111111111111111111111111111111111111111111111111", TbUtils.intLongToString(Long.MAX_VALUE, MIN_RADIX));
Assertions.assertEquals("1000000000000000000000000000000000000000000000000000000000000001", TbUtils.intLongToString(-Long.MAX_VALUE, MIN_RADIX));
Assertions.assertEquals("1111111111111111111111111111111111111111111111111111111110011010", TbUtils.intLongToString(-102L, MIN_RADIX));
Assertions.assertEquals("1111111111111111111111111111111111111111111111111100110010011010", TbUtils.intLongToString(-13158L, MIN_RADIX));
Assertions.assertEquals("777777777777777777777", TbUtils.intLongToString(Long.MAX_VALUE, 8));
Assertions.assertEquals("1000000000000000000000", TbUtils.intLongToString(Long.MIN_VALUE, 8));
Assertions.assertEquals("9223372036854775807", TbUtils.intLongToString(Long.MAX_VALUE));
Assertions.assertEquals("-9223372036854775808", TbUtils.intLongToString(Long.MIN_VALUE));
Assertions.assertEquals("3366", TbUtils.intLongToString(13158L, 16));
Assertions.assertEquals("FFCC9A", TbUtils.intLongToString(-13158L, 16));
Assertions.assertEquals("0xFFCC9A", TbUtils.intLongToString(-13158L, 16, true, true));
Assertions.assertEquals("0x0400", TbUtils.intLongToString(1024L, 16, true, true));
Assertions.assertNotEquals("400", TbUtils.intLongToString(1024L, 16));
Assertions.assertEquals("0xFFFC00", TbUtils.intLongToString(-1024L, 16, true, true));
Assertions.assertNotEquals("0xFC00", TbUtils.intLongToString(-1024L, 16, true, true));
Assertions.assertEquals("hazelnut", TbUtils.intLongToString(1356099454469L, MAX_RADIX));
Assertions.assertEquals("00111010", TbUtils.intLongToRadixString(58L, MIN_RADIX));
Assertions.assertEquals("0000000010011110", TbUtils.intLongToRadixString(158L, MIN_RADIX));
Assertions.assertEquals("00000000000000100000001000000001", TbUtils.intLongToRadixString(131585L, MIN_RADIX));
Assertions.assertEquals("0111111111111111111111111111111111111111111111111111111111111111", TbUtils.intLongToRadixString(Long.MAX_VALUE, MIN_RADIX));
Assertions.assertEquals("1000000000000000000000000000000000000000000000000000000000000001", TbUtils.intLongToRadixString(-Long.MAX_VALUE, MIN_RADIX));
Assertions.assertEquals("1111111111111111111111111111111111111111111111111111111110011010", TbUtils.intLongToRadixString(-102L, MIN_RADIX));
Assertions.assertEquals("1111111111111111111111111111111111111111111111111100110010011010", TbUtils.intLongToRadixString(-13158L, MIN_RADIX));
Assertions.assertEquals("777777777777777777777", TbUtils.intLongToRadixString(Long.MAX_VALUE, 8));
Assertions.assertEquals("1000000000000000000000", TbUtils.intLongToRadixString(Long.MIN_VALUE, 8));
Assertions.assertEquals("9223372036854775807", TbUtils.intLongToRadixString(Long.MAX_VALUE));
Assertions.assertEquals("-9223372036854775808", TbUtils.intLongToRadixString(Long.MIN_VALUE));
Assertions.assertEquals("3366", TbUtils.intLongToRadixString(13158L, 16));
Assertions.assertEquals("FFCC9A", TbUtils.intLongToRadixString(-13158L, 16));
Assertions.assertEquals("0xFFCC9A", TbUtils.intLongToRadixString(-13158L, 16, true, true));
Assertions.assertEquals("0x0400", TbUtils.intLongToRadixString(1024L, 16, true, true));
Assertions.assertNotEquals("400", TbUtils.intLongToRadixString(1024L, 16));
Assertions.assertEquals("0xFFFC00", TbUtils.intLongToRadixString(-1024L, 16, true, true));
Assertions.assertNotEquals("0xFC00", TbUtils.intLongToRadixString(-1024L, 16, true, true));
Assertions.assertEquals("hazelnut", TbUtils.intLongToRadixString(1356099454469L, MAX_RADIX));
}
@Test
@ -870,14 +886,16 @@ public class TbUtilsTest {
@Test
public void raiseError_Test() {
String message = "frequency_weighting_type must be 0, 1 or 2.";
Object value = 4;
String message = "frequency_weighting_type must be 0, 1 or 2. A value of " + value.toString() + " is invalid.";
try {
TbUtils.raiseError(message, value);
TbUtils.raiseError(message);
Assertions.fail("Should throw NumberFormatException");
} catch (RuntimeException e) {
Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. for value 4"));
Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. A value of 4 is invalid."));
}
message = "frequency_weighting_type must be 0, 1 or 2.";
try {
TbUtils.raiseError(message);
Assertions.fail("Should throw NumberFormatException");
@ -1067,6 +1085,18 @@ public class TbUtilsTest {
String actual = TbUtils.hexToBase64(hex);
Assertions.assertEquals(expected, actual);
}
@Test
public void bytesToHex_Test() {
byte[] bb = {(byte) 0xBB, (byte) 0xAA};
String expected = "BBAA";
String actual = TbUtils.bytesToHex(bb);
Assertions.assertEquals(expected, actual);
ExecutionArrayList<Integer> expectedList = new ExecutionArrayList<>(ctx);
expectedList.addAll(List.of(-69, 83));
expected = "BB53";
actual = TbUtils.bytesToHex(expectedList);
Assertions.assertEquals(expected, actual);
}
private static List<Byte> toList(byte[] data) {
List<Byte> result = new ArrayList<>(data.length);

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

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

5
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java

@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.transport.TransportContext;
import org.thingsboard.server.common.transport.TransportTenantProfileCache;
import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor;
import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor;
@ -51,6 +52,10 @@ public class MqttTransportContext extends TransportContext {
@Autowired
private ProtoMqttAdaptor protoMqttAdaptor;
@Getter
@Autowired
private TransportTenantProfileCache tenantProfileCache;
@Getter
@Value("${transport.mqtt.netty.max_payload_size}")
private Integer maxPayloadSize;

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

@ -60,6 +60,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
@ -78,6 +79,8 @@ import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto;
import org.thingsboard.server.queue.scheduler.SchedulerComponent;
import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor;
import org.thingsboard.server.transport.mqtt.limits.GatewaySessionLimits;
import org.thingsboard.server.transport.mqtt.limits.SessionLimits;
import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx;
import org.thingsboard.server.transport.mqtt.session.GatewaySessionHandler;
import org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher;
@ -130,6 +133,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
private static final Pattern FW_REQUEST_PATTERN = Pattern.compile(MqttTopics.DEVICE_FIRMWARE_REQUEST_TOPIC_PATTERN);
private static final Pattern SW_REQUEST_PATTERN = Pattern.compile(MqttTopics.DEVICE_SOFTWARE_REQUEST_TOPIC_PATTERN);
private static final String SESSION_LIMITS = "getSessionLimits";
private static final String PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE";
@ -493,8 +497,12 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
transportService.process(deviceSessionCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(ctx, msgId, rpcResponseMsg));
} else if (topicName.startsWith(MqttTopics.DEVICE_RPC_REQUESTS_TOPIC)) {
TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = payloadAdaptor.convertToServerRpcRequest(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC);
transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg));
toServerRpcSubTopicType = TopicType.V1;
if (SESSION_LIMITS.equals(rpcRequestMsg.getMethodName())) {
onGetSessionLimitsRpc(deviceSessionCtx.getSessionInfo(), ctx, msgId, rpcRequestMsg);
} else {
transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg));
}
} else if (topicName.equals(MqttTopics.DEVICE_CLAIM_TOPIC)) {
TransportProtos.ClaimDeviceMsg claimDeviceMsg = payloadAdaptor.convertToClaimDevice(deviceSessionCtx, mqttMsg);
transportService.process(deviceSessionCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(ctx, msgId, claimDeviceMsg));
@ -928,7 +936,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
} else {
log.debug("[{}] Failed to process unsubscription [{}] to [{}] - Subscription not found", sessionId, mqttMsg.variableHeader().messageId(), topicName);
unSubResults.add((short)MqttReasonCodes.UnsubAck.NO_SUBSCRIPTION_EXISTED.byteValue());
unSubResults.add((short) MqttReasonCodes.UnsubAck.NO_SUBSCRIPTION_EXISTED.byteValue());
}
}
if (!activityReported) {
@ -1055,6 +1063,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
log.debug("[{}][{}][{}] IOException: {}", sessionId,
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null),
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""),
cause.getMessage(),
cause);
} else if (log.isInfoEnabled()) {
log.info("[{}][{}][{}] IOException: {}", sessionId,
@ -1330,6 +1339,43 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
}
private void onGetSessionLimitsRpc(TransportProtos.SessionInfoProto sessionInfo, ChannelHandlerContext ctx, int msgId, TransportProtos.ToServerRpcRequestMsg rpcRequestMsg) {
var tenantProfile = context.getTenantProfileCache().get(deviceSessionCtx.getTenantId());
DefaultTenantProfileConfiguration profile = tenantProfile.getDefaultProfileConfiguration();
SessionLimits sessionLimits;
if (sessionInfo.getIsGateway()) {
var gatewaySessionLimits = new GatewaySessionLimits();
var gatewayLimits = new SessionLimits.SessionRateLimits(profile.getTransportGatewayMsgRateLimit(),
profile.getTransportGatewayTelemetryMsgRateLimit(),
profile.getTransportGatewayTelemetryDataPointsRateLimit());
var gatewayDeviceLimits = new SessionLimits.SessionRateLimits(profile.getTransportGatewayDeviceMsgRateLimit(),
profile.getTransportGatewayDeviceTelemetryMsgRateLimit(),
profile.getTransportGatewayDeviceTelemetryDataPointsRateLimit());
gatewaySessionLimits.setGatewayRateLimits(gatewayLimits);
gatewaySessionLimits.setRateLimits(gatewayDeviceLimits);
sessionLimits = gatewaySessionLimits;
} else {
var rateLimits = new SessionLimits.SessionRateLimits(profile.getTransportDeviceMsgRateLimit(),
profile.getTransportDeviceTelemetryMsgRateLimit(),
profile.getTransportDeviceTelemetryDataPointsRateLimit());
sessionLimits = new SessionLimits();
sessionLimits.setRateLimits(rateLimits);
}
sessionLimits.setMaxPayloadSize(context.getMaxPayloadSize());
sessionLimits.setMaxInflightMessages(context.getMessageQueueSizePerDeviceLimit());
ack(ctx, msgId, MqttReasonCodes.PubAck.SUCCESS);
TransportProtos.ToServerRpcResponseMsg responseMsg = TransportProtos.ToServerRpcResponseMsg.newBuilder()
.setRequestId(rpcRequestMsg.getRequestId())
.setPayload(JacksonUtil.toString(sessionLimits))
.build();
onToServerRpcResponse(responseMsg);
}
private void handleToSparkplugDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg rpcRequest) throws ThingsboardException {
SparkplugMessageType messageType = SparkplugMessageType.parseMessageType(rpcRequest.getMethodName());
SparkplugRpcRequestHeader header;

25
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/limits/GatewaySessionLimits.java

@ -0,0 +1,25 @@
/**
* 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.transport.mqtt.limits;
import lombok.Data;
@Data
public class GatewaySessionLimits extends SessionLimits {
private SessionRateLimits gatewayRateLimits;
}

29
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/limits/SessionLimits.java

@ -0,0 +1,29 @@
/**
* 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.transport.mqtt.limits;
import lombok.Data;
import org.thingsboard.server.common.data.TransportPayloadType;
@Data
public class SessionLimits {
private int maxPayloadSize;
private int maxInflightMessages;
private SessionRateLimits rateLimits;
public record SessionRateLimits(String messages, String telemetryMessages, String telemetryDataPoints) {}
}

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

@ -82,6 +82,7 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
private volatile MqttTopicFilter telemetryTopicFilter = MqttTopicFilterFactory.getDefaultTelemetryFilter();
private volatile MqttTopicFilter attributesPublishTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter();
private volatile MqttTopicFilter attributesSubscribeTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter();
@Getter
private volatile TransportPayloadType payloadType = TransportPayloadType.JSON;
private volatile Descriptors.Descriptor attributesDynamicMessageDescriptor;
private volatile Descriptors.Descriptor telemetryDynamicMessageDescriptor;

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

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

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

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

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

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

5
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
@ -40,6 +41,8 @@ public abstract class DeviceAwareSessionContext implements SessionContext {
@Getter
private volatile DeviceId deviceId;
@Getter
private volatile TenantId tenantId;
@Getter
protected volatile TransportDeviceInfo deviceInfo;
@Getter
@Setter
@ -58,6 +61,7 @@ public abstract class DeviceAwareSessionContext implements SessionContext {
public void setDeviceInfo(TransportDeviceInfo deviceInfo) {
this.deviceInfo = deviceInfo;
this.deviceId = deviceInfo.getDeviceId();
this.tenantId = deviceInfo.getTenantId();
}
@Override
@ -65,7 +69,6 @@ public abstract class DeviceAwareSessionContext implements SessionContext {
this.sessionInfo = sessionInfo;
this.deviceProfile = deviceProfile;
this.deviceInfo.setDeviceType(deviceProfile.getName());
}
@Override

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.device;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -27,6 +28,7 @@ import org.springframework.util.CollectionUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.device.DeviceCacheEvictEvent;
import org.thingsboard.server.cache.device.DeviceCacheKey;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.DeviceInfo;
@ -554,6 +556,11 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
device.setName(provisionRequest.getDeviceName());
device.setType(profile.getName());
device.setTenantId(profile.getTenantId());
if (provisionRequest.getGateway() != null && provisionRequest.getGateway()) {
ObjectNode additionalInfoNode = JacksonUtil.newObjectNode();
additionalInfoNode.put(DataConstants.GATEWAY_PARAMETER, true);
device.setAdditionalInfo(additionalInfoNode);
}
Device savedDevice = saveDevice(device);
if (!StringUtils.isEmpty(provisionRequest.getCredentialsData().getToken()) ||
!StringUtils.isEmpty(provisionRequest.getCredentialsData().getX509CertHash()) ||

2
dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java

@ -36,6 +36,7 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.oauth2.OAuth2ClientDao;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -149,6 +150,7 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe
}
List<OAuth2ClientInfo> clients = oauth2ClientDao.findByDomainId(domain.getUuidId()).stream()
.map(OAuth2ClientInfo::new)
.sorted(Comparator.comparing(OAuth2ClientInfo::getTitle))
.collect(Collectors.toList());
return new DomainInfo(domain, clients);
}

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

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

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

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

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

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

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

Loading…
Cancel
Save