Browse Source

Merge remote-tracking branch 'upstream/master' into bug/latest-values-map-with-key-value-agg

pull/14685/head
Ekaterina Chantsova 11 months ago
parent
commit
13712911bc
  1. 14
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 19
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  3. 8
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldException.java
  4. 133
      application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java
  5. 2
      application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java
  7. 2
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java
  8. 45
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java
  9. 4
      application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java
  10. 13
      application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java
  11. 8
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  12. 6
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  13. 3
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
  14. 28
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java
  15. 100
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java
  16. 5
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  17. 24
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  18. 26
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  19. 34
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java
  20. 14
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java
  21. 14
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java
  22. 17
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java
  23. 13
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java
  24. 15
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java
  25. 13
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java
  26. 34
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java
  27. 155
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java
  28. 101
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java
  29. 31
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserProcessor.java
  30. 13
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  31. 2
      application/src/main/java/org/thingsboard/server/service/user/cache/DefaultUserAuthDetailsCache.java
  32. 16
      application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java
  33. 23
      application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
  34. 11
      application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
  35. 15
      application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java
  36. 393
      application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java
  37. 6
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  38. 3
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  39. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java
  40. 1
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java
  41. 2
      common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java
  42. 22
      common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalTest.java
  43. 2
      common/edge-api/src/main/proto/edge.proto
  44. 38
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  45. 4
      ui-ngx/src/app/core/auth/auth.service.ts
  46. 5
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  47. 16
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  48. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  49. 10
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
  50. 23
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts
  51. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts
  52. 5
      ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html
  53. 4
      ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.html
  54. 17
      ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.scss
  55. 5
      ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.ts
  56. 5
      ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html
  57. 4
      ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.ts
  58. 2
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts
  59. 38
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html
  60. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts
  61. 7
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  62. 6
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  63. 14
      ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html
  64. 4
      ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts
  65. 10
      ui-ngx/src/app/modules/home/pages/security/security.component.html
  66. 82
      ui-ngx/src/app/modules/home/pages/security/security.component.ts
  67. 34
      ui-ngx/src/app/modules/login/login-routing.module.ts
  68. 22
      ui-ngx/src/app/modules/login/pages/login/create-password.component.html
  69. 60
      ui-ngx/src/app/modules/login/pages/login/create-password.component.ts
  70. 40
      ui-ngx/src/app/modules/login/pages/login/login.component.html
  71. 36
      ui-ngx/src/app/modules/login/pages/login/login.component.scss
  72. 20
      ui-ngx/src/app/modules/login/pages/login/reset-password.component.html
  73. 60
      ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts
  74. 35
      ui-ngx/src/app/shared/components/password-requirements-tooltip.component.html
  75. 48
      ui-ngx/src/app/shared/components/password-requirements-tooltip.component.scss
  76. 53
      ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts
  77. 2
      ui-ngx/src/app/shared/models/api-key.models.ts
  78. 1
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  79. 106
      ui-ngx/src/app/shared/models/password.models.ts
  80. 1
      ui-ngx/src/app/shared/models/public-api.ts
  81. 5
      ui-ngx/src/app/shared/models/time/time.models.ts
  82. 11
      ui-ngx/src/app/shared/shared.module.ts
  83. 23
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.actors; package org.thingsboard.server.actors;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -54,7 +55,6 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -119,7 +119,6 @@ import org.thingsboard.server.service.cf.CalculatedFieldProcessingService;
import org.thingsboard.server.service.cf.CalculatedFieldQueueService; import org.thingsboard.server.service.cf.CalculatedFieldQueueService;
import org.thingsboard.server.service.cf.CalculatedFieldStateService; import org.thingsboard.server.service.cf.CalculatedFieldStateService;
import org.thingsboard.server.service.cf.OwnerService; import org.thingsboard.server.service.cf.OwnerService;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService;
@ -144,14 +143,12 @@ import org.thingsboard.server.utils.DebugModeRateLimitsConfig;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@Component @Component
@ -842,7 +839,7 @@ public class ActorSystemContext {
Futures.addCallback(future, RULE_CHAIN_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor()); Futures.addCallback(future, RULE_CHAIN_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor());
} }
public void persistCalculatedFieldDebugEvent(TenantId tenantId, CalculatedFieldId calculatedFieldId, EntityId entityId, Map<String, ArgumentEntry> arguments, UUID tbMsgId, TbMsgType tbMsgType, String result, String errorMessage) { public void persistCalculatedFieldDebugEvent(TenantId tenantId, CalculatedFieldId calculatedFieldId, EntityId entityId, JsonNode arguments, UUID tbMsgId, String tbMsgType, String result, String errorMessage) {
if (checkLimits(tenantId)) { if (checkLimits(tenantId)) {
try { try {
CalculatedFieldDebugEvent.CalculatedFieldDebugEventBuilder eventBuilder = CalculatedFieldDebugEvent.builder() CalculatedFieldDebugEvent.CalculatedFieldDebugEventBuilder eventBuilder = CalculatedFieldDebugEvent.builder()
@ -855,13 +852,10 @@ public class ActorSystemContext {
eventBuilder.msgId(tbMsgId); eventBuilder.msgId(tbMsgId);
} }
if (tbMsgType != null) { if (tbMsgType != null) {
eventBuilder.msgType(tbMsgType.name()); eventBuilder.msgType(tbMsgType);
} }
if (arguments != null) { if (arguments != null) {
eventBuilder.arguments(JacksonUtil.toString( eventBuilder.arguments(JacksonUtil.toString(arguments));
arguments.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().jsonValue()))
));
} }
if (result != null) { if (result != null) {
eventBuilder.result(result); eventBuilder.result(result);

19
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java

@ -70,6 +70,7 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.DataConstants.REEVALUATION_MSG;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType;
/** /**
@ -311,6 +312,9 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
} catch (Exception e) { } catch (Exception e) {
log.debug("[{}][{}] Failed to process linked CF telemetry msg: {}", entityId, ctx.getCfId(), msg, e); log.debug("[{}][{}] Failed to process linked CF telemetry msg: {}", entityId, ctx.getCfId(), msg, e);
if (e instanceof CalculatedFieldException cfe) {
throw cfe;
}
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
} }
} }
@ -351,7 +355,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
if (state.isSizeOk()) { if (state.isSizeOk()) {
log.debug("[{}][{}] Reevaluating CF state", entityId, cfId); log.debug("[{}][{}] Reevaluating CF state", entityId, cfId);
processStateIfReady(state, null, ctx, Collections.singletonList(cfId), null, null, msg.getCallback()); processStateIfReady(state, null, ctx, Collections.singletonList(cfId), null, REEVALUATION_MSG, msg.getCallback());
} else { } else {
throw new RuntimeException(ctx.getSizeExceedsLimitMessage()); throw new RuntimeException(ctx.getSizeExceedsLimitMessage());
} }
@ -432,7 +436,8 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (!updatedArgs.isEmpty() || justRestored) { if (!updatedArgs.isEmpty() || justRestored) {
cfIdList = new ArrayList<>(cfIdList); cfIdList = new ArrayList<>(cfIdList);
cfIdList.add(ctx.getCfId()); cfIdList.add(ctx.getCfId());
processStateIfReady(state, updatedArgs, ctx, cfIdList, tbMsgId, tbMsgType, callback); String msgType = tbMsgType == null ? null : tbMsgType.name();
processStateIfReady(state, updatedArgs, ctx, cfIdList, tbMsgId, msgType, callback);
} else { } else {
callback.onSuccess(); callback.onSuccess();
} }
@ -474,7 +479,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
private void processStateIfReady(CalculatedFieldState state, Map<String, ArgumentEntry> updatedArgs, CalculatedFieldCtx ctx, private void processStateIfReady(CalculatedFieldState state, Map<String, ArgumentEntry> updatedArgs, CalculatedFieldCtx ctx,
List<CalculatedFieldId> cfIdList, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { List<CalculatedFieldId> cfIdList, UUID tbMsgId, String tbMsgType, TbCallback callback) throws CalculatedFieldException {
callback = new MultipleTbCallback(CALLBACKS_PER_CF, callback); callback = new MultipleTbCallback(CALLBACKS_PER_CF, callback);
log.trace("[{}][{}] Processing state if ready. Current args: {}, updated args: {}", entityId, ctx.getCfId(), state.getArguments(), updatedArgs); log.trace("[{}][{}] Processing state if ready. Current args: {}, updated args: {}", entityId, ctx.getCfId(), state.getArguments(), updatedArgs);
CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId); CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId);
@ -487,24 +492,24 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
stateSizeChecked = true; stateSizeChecked = true;
if (state.isSizeOk()) { if (state.isSizeOk()) {
if (!calculationResult.isEmpty()) { if (!calculationResult.isEmpty()) {
cfService.processResult(tenantId, entityId, calculationResult, cfIdList, callback); cfService.processResult(tenantId, entityId, ctx.getCfName(), calculationResult, cfIdList, callback);
} else { } else {
callback.onSuccess(); callback.onSuccess();
} }
if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) { if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) {
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, calculationResult.stringValue(), null); systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArgumentsJson(), tbMsgId, tbMsgType, calculationResult.stringValue(), null);
} }
} }
} else { } else {
if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) {
String errorMsg = ctx.isInitialized() ? state.getReadinessStatus().errorMsg() : "Calculated field state is not initialized!"; String errorMsg = ctx.isInitialized() ? state.getReadinessStatus().errorMsg() : "Calculated field state is not initialized!";
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, null, errorMsg); systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArgumentsJson(), tbMsgId, tbMsgType, null, errorMsg);
} }
callback.onSuccess(); callback.onSuccess();
} }
} catch (Exception e) { } catch (Exception e) {
log.debug("[{}][{}] Failed to process CF state", entityId, ctx.getCfId(), e); log.debug("[{}][{}] Failed to process CF state", entityId, ctx.getCfId(), e);
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build(); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArgumentsJson()).cause(e).build();
} finally { } finally {
if (!stateSizeChecked) { if (!stateSizeChecked) {
state.checkStateSize(ctxId, ctx.getMaxStateSize()); state.checkStateSize(ctxId, ctx.getMaxStateSize());

8
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldException.java

@ -15,14 +15,12 @@
*/ */
package org.thingsboard.server.actors.calculatedField; package org.thingsboard.server.actors.calculatedField;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
@Getter @Getter
@ -32,8 +30,8 @@ public class CalculatedFieldException extends Exception {
private final CalculatedFieldCtx ctx; private final CalculatedFieldCtx ctx;
private final EntityId eventEntity; private final EntityId eventEntity;
private final UUID msgId; private final UUID msgId;
private final TbMsgType msgType; private final String msgType;
private Map<String, ArgumentEntry> arguments; private JsonNode arguments;
private String errorMessage; private String errorMessage;
private Exception cause; private Exception cause;

133
application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java

@ -15,12 +15,12 @@
*/ */
package org.thingsboard.server.service.cf; package org.thingsboard.server.service.cf;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
@ -28,10 +28,12 @@ import jakarta.annotation.PreDestroy;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.rule.engine.api.AttributesSaveRequest; import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.rule.engine.api.AttributesSaveRequest.Strategy; import org.thingsboard.rule.engine.api.AttributesSaveRequest.Strategy;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
@ -62,11 +64,15 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.EntityRelationPathQuery;
import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
@ -85,10 +91,13 @@ import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.DataConstants.CF_NAME_METADATA_KEY;
import static org.thingsboard.server.common.data.DataConstants.SCOPE;
import static org.thingsboard.server.common.data.cf.CalculatedFieldType.PROPAGATION; import static org.thingsboard.server.common.data.cf.CalculatedFieldType.PROPAGATION;
import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY;
import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED;
import static org.thingsboard.server.dao.util.KvUtils.filterChangedAttr; import static org.thingsboard.server.dao.util.KvUtils.filterChangedAttr;
import static org.thingsboard.server.dao.util.KvUtils.toTsKvEntryList; import static org.thingsboard.server.dao.util.KvUtils.toTsKvEntryList;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultAttributeEntry; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultAttributeEntry;
@ -108,6 +117,7 @@ public abstract class AbstractCalculatedFieldProcessingService {
protected final ApiLimitService apiLimitService; protected final ApiLimitService apiLimitService;
protected final RelationService relationService; protected final RelationService relationService;
protected final OwnerService ownerService; protected final OwnerService ownerService;
protected final TbClusterService clusterService;
protected ListeningExecutorService calculatedFieldCallbackExecutor; protected ListeningExecutorService calculatedFieldCallbackExecutor;
@ -392,42 +402,46 @@ public abstract class AbstractCalculatedFieldProcessingService {
return new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, endTs, 0, limit, Aggregation.NONE); return new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, endTs, 0, limit, Aggregation.NONE);
} }
protected void saveTelemetryResult(TenantId tenantId, EntityId entityId, TelemetryCalculatedFieldResult cfResult, List<CalculatedFieldId> cfIds, TbCallback callback) { protected void sendMsgToRuleEngine(TenantId tenantId, EntityId entityId, TbCallback callback, TbMsg msg) {
try {
clusterService.pushMsgToRuleEngine(tenantId, entityId, msg, new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
log.trace("[{}][{}] Pushed message to rule engine: {} ", tenantId, entityId, msg);
callback.onSuccess();
}
@Override
public void onFailure(Throwable t) {
callback.onFailure(t);
}
});
} catch (Exception e) {
log.warn("[{}][{}] Failed to push message to rule engine: {}", tenantId, entityId, msg, e);
callback.onFailure(e);
}
}
protected void saveTelemetryResult(TenantId tenantId, EntityId entityId, String cfName, TelemetryCalculatedFieldResult cfResult, List<CalculatedFieldId> cfIds, TbCallback callback) {
OutputType type = cfResult.getType(); OutputType type = cfResult.getType();
JsonElement jsonResult = JsonParser.parseString(Objects.requireNonNull(cfResult.stringValue())); JsonElement jsonResult = JsonParser.parseString(Objects.requireNonNull(cfResult.stringValue()));
log.trace("[{}][{}] Saving CF result: {}", tenantId, entityId, jsonResult); log.trace("[{}][{}] Saving CF result: {}", tenantId, entityId, jsonResult);
SettableFuture<Void> future = SettableFuture.create();
switch (type) { switch (type) {
case ATTRIBUTES -> saveAttributes(tenantId, entityId, jsonResult, cfResult.getOutputStrategy(), cfResult.getScope(), cfIds, future); case ATTRIBUTES -> saveAttributes(tenantId, entityId, jsonResult, cfResult.getOutputStrategy(), cfResult.getScope(), cfName, cfIds, callback);
case TIME_SERIES -> saveTimeSeries(tenantId, entityId, jsonResult, cfResult.getOutputStrategy(), cfIds, System.currentTimeMillis(), future); case TIME_SERIES -> saveTimeSeries(tenantId, entityId, jsonResult, cfResult.getOutputStrategy(), cfIds, System.currentTimeMillis(), callback);
} }
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(Void v) {
callback.onSuccess();
log.debug("[{}][{}] Saved CF result: {}", tenantId, entityId, cfResult);
}
@Override
public void onFailure(Throwable t) {
callback.onFailure(t);
log.error("[{}][{}] Failed to save CF result {}", tenantId, entityId, cfResult, t);
}
}, MoreExecutors.directExecutor());
} }
private void saveAttributes(TenantId tenantId, EntityId entityId, JsonElement jsonResult, OutputStrategy outputStrategy, AttributeScope scope, List<CalculatedFieldId> cfIds, SettableFuture<Void> future) { private void saveAttributes(TenantId tenantId, EntityId entityId, JsonElement jsonResult, OutputStrategy outputStrategy, AttributeScope scope, String cfName, List<CalculatedFieldId> cfIds, TbCallback callback) {
if (!(outputStrategy instanceof AttributesImmediateOutputStrategy attOutputStrategy)) { if (!(outputStrategy instanceof AttributesImmediateOutputStrategy attOutputStrategy)) {
future.setException(new IllegalArgumentException("Only AttributeImmediateOutputStrategy is supported.")); callback.onFailure(new IllegalArgumentException("Only AttributeImmediateOutputStrategy is supported."));
} else { } else {
AttributesSaveRequest.Strategy strategy = new Strategy(attOutputStrategy.isSaveAttribute(), attOutputStrategy.isSendWsUpdate(), attOutputStrategy.isProcessCfs()); AttributesSaveRequest.Strategy strategy = new Strategy(attOutputStrategy.isSaveAttribute(), attOutputStrategy.isSendWsUpdate(), attOutputStrategy.isProcessCfs());
List<AttributeKvEntry> newAttributes = JsonConverter.convertToAttributes(jsonResult); List<AttributeKvEntry> newAttributes = JsonConverter.convertToAttributes(jsonResult);
if (!attOutputStrategy.isUpdateAttributesOnlyOnValueChange()) { if (!attOutputStrategy.isUpdateAttributesOnlyOnValueChange()) {
saveAttributesInternal(tenantId, entityId, scope, cfIds, newAttributes, strategy, future); saveAttributesInternal(tenantId, entityId, scope, cfName, cfIds, newAttributes, strategy, attOutputStrategy.isSendAttributesUpdatedNotification(), callback);
return; return;
} }
@ -438,22 +452,24 @@ public abstract class AbstractCalculatedFieldProcessingService {
existingAttributes -> { existingAttributes -> {
List<AttributeKvEntry> changed = filterChangedAttr(existingAttributes, newAttributes); List<AttributeKvEntry> changed = filterChangedAttr(existingAttributes, newAttributes);
if (changed.isEmpty()) { if (changed.isEmpty()) {
future.set(null); callback.onSuccess();
return; return;
} }
saveAttributesInternal(tenantId, entityId, scope, cfIds, changed, strategy, future); saveAttributesInternal(tenantId, entityId, scope, cfName, cfIds, changed, strategy, attOutputStrategy.isSendAttributesUpdatedNotification(), callback);
}, },
future::setException, callback::onFailure,
MoreExecutors.directExecutor()); MoreExecutors.directExecutor());
} }
} }
private void saveAttributesInternal(TenantId tenantId, EntityId entityId, private void saveAttributesInternal(TenantId tenantId, EntityId entityId,
AttributeScope scope, AttributeScope scope,
String cfName,
List<CalculatedFieldId> cfIds, List<CalculatedFieldId> cfIds,
List<AttributeKvEntry> entries, List<AttributeKvEntry> entries,
AttributesSaveRequest.Strategy strategy, AttributesSaveRequest.Strategy strategy,
SettableFuture<Void> future) { boolean sendAttributesUpdatedNotification,
TbCallback callback) {
tsSubService.saveAttributes(AttributesSaveRequest.builder() tsSubService.saveAttributes(AttributesSaveRequest.builder()
.tenantId(tenantId) .tenantId(tenantId)
.entityId(entityId) .entityId(entityId)
@ -461,23 +477,38 @@ public abstract class AbstractCalculatedFieldProcessingService {
.entries(entries) .entries(entries)
.strategy(strategy) .strategy(strategy)
.previousCalculatedFieldIds(cfIds) .previousCalculatedFieldIds(cfIds)
.future(future) .callback(new FutureCallback<>() {
@Override
public void onSuccess(Void result) {
if (sendAttributesUpdatedNotification) {
sendAttributesUpdatedMsg(tenantId, entityId, scope, cfName, entries);
}
callback.onSuccess();
log.debug("[{}][{}] Saved CF result: {}", tenantId, entityId, entries);
}
@Override
public void onFailure(Throwable t) {
callback.onFailure(t);
log.error("[{}][{}] Failed to save CF result {}", tenantId, entityId, entries, t);
}
})
.build()); .build());
} }
private void saveTimeSeries(TenantId tenantId, EntityId entityId, JsonElement jsonResult, OutputStrategy outputStrategy, List<CalculatedFieldId> cfIds, long ts, SettableFuture<Void> future) { private void saveTimeSeries(TenantId tenantId, EntityId entityId, JsonElement jsonResult, OutputStrategy outputStrategy, List<CalculatedFieldId> cfIds, long ts, TbCallback callback) {
if (!(outputStrategy instanceof TimeSeriesImmediateOutputStrategy tsOutputStrategy)) { if (!(outputStrategy instanceof TimeSeriesImmediateOutputStrategy tsOutputStrategy)) {
future.setException(new IllegalArgumentException("Only TimeSeriesImmediateOutputStrategy is supported.")); callback.onFailure(new IllegalArgumentException("Only TimeSeriesImmediateOutputStrategy is supported."));
} else { } else {
TimeseriesSaveRequest.Strategy strategy = new TimeseriesSaveRequest.Strategy(tsOutputStrategy.isSaveTimeSeries(), tsOutputStrategy.isSaveLatest(), tsOutputStrategy.isSendWsUpdate(), tsOutputStrategy.isProcessCfs()); TimeseriesSaveRequest.Strategy strategy = new TimeseriesSaveRequest.Strategy(tsOutputStrategy.isSaveTimeSeries(), tsOutputStrategy.isSaveLatest(), tsOutputStrategy.isSendWsUpdate(), tsOutputStrategy.isProcessCfs());
saveTimeSeriesInternal(tenantId, entityId, jsonResult, tsOutputStrategy.getTtl(), cfIds, ts, strategy, future); saveTimeSeriesInternal(tenantId, entityId, jsonResult, tsOutputStrategy.getTtl(), cfIds, ts, strategy, callback);
} }
} }
private void saveTimeSeriesInternal(TenantId tenantId, EntityId entityId, JsonElement jsonResult, Long ttl, List<CalculatedFieldId> cfIds, long ts, TimeseriesSaveRequest.Strategy strategy, SettableFuture<Void> future) { private void saveTimeSeriesInternal(TenantId tenantId, EntityId entityId, JsonElement jsonResult, Long ttl, List<CalculatedFieldId> cfIds, long ts, TimeseriesSaveRequest.Strategy strategy, TbCallback callback) {
Map<Long, List<KvEntry>> tsKvMap = JsonConverter.convertToTelemetry(jsonResult, ts); Map<Long, List<KvEntry>> tsKvMap = JsonConverter.convertToTelemetry(jsonResult, ts);
if (tsKvMap.isEmpty()) { if (tsKvMap.isEmpty()) {
future.set(null); callback.onSuccess();
return; return;
} }
List<TsKvEntry> tsEntries = toTsKvEntryList(tsKvMap); List<TsKvEntry> tsEntries = toTsKvEntryList(tsKvMap);
@ -486,7 +517,19 @@ public abstract class AbstractCalculatedFieldProcessingService {
.entityId(entityId) .entityId(entityId)
.entries(tsEntries) .entries(tsEntries)
.strategy(strategy) .strategy(strategy)
.future(future); .callback(new FutureCallback<>() {
@Override
public void onSuccess(Void result) {
callback.onSuccess();
log.debug("[{}][{}] Saved CF result: {}", tenantId, entityId, tsEntries);
}
@Override
public void onFailure(Throwable t) {
callback.onFailure(t);
log.error("[{}][{}] Failed to save CF result {}", tenantId, entityId, tsEntries, t);
}
});
if (ttl != null) { if (ttl != null) {
builder.ttl(ttl); builder.ttl(ttl);
} }
@ -496,4 +539,26 @@ public abstract class AbstractCalculatedFieldProcessingService {
tsSubService.saveTimeseries(builder.build()); tsSubService.saveTimeseries(builder.build());
} }
private void sendAttributesUpdatedMsg(TenantId tenantId, EntityId entityId,
AttributeScope scope,
String cfName,
List<AttributeKvEntry> entries) {
ObjectNode entityNode = JacksonUtil.newObjectNode();
if (entries != null) {
entries.forEach(attributeKvEntry -> JacksonUtil.addKvEntry(entityNode, attributeKvEntry));
}
TbMsg attributesUpdatedMsg = TbMsg.newMsg()
.type(ATTRIBUTES_UPDATED)
.originator(entityId)
.data(JacksonUtil.toString(entityNode))
.metaData(new TbMsgMetaData(Map.of(
CF_NAME_METADATA_KEY, cfName,
SCOPE, scope.name()
)))
.build();
sendMsgToRuleEngine(tenantId, entityId, TbCallback.EMPTY, attributesUpdatedMsg);
}
} }

2
application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java

@ -37,7 +37,7 @@ public class AlarmCalculatedFieldResult implements CalculatedFieldResult {
private final TbAlarmResult alarmResult; private final TbAlarmResult alarmResult;
@Override @Override
public TbMsg toTbMsg(EntityId entityId, List<CalculatedFieldId> cfIds) { public TbMsg toTbMsg(EntityId entityId, String cfName, List<CalculatedFieldId> cfIds) {
TbMsgType msgType; TbMsgType msgType;
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
if (alarmResult.isCreated()) { if (alarmResult.isCreated()) {

2
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java

@ -41,7 +41,7 @@ public interface CalculatedFieldProcessingService {
Map<String, ArgumentEntry> fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map<String, Argument> arguments); Map<String, ArgumentEntry> fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map<String, Argument> arguments);
void processResult(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List<CalculatedFieldId> cfIds, TbCallback callback); void processResult(TenantId tenantId, EntityId entityId, String cfName, CalculatedFieldResult result, List<CalculatedFieldId> cfIds, TbCallback callback);
ArgumentEntry fetchMetricDuringInterval(TenantId tenantId, EntityId entityId, String argKey, AggMetric metric, AggIntervalEntry interval); ArgumentEntry fetchMetricDuringInterval(TenantId tenantId, EntityId entityId, String argKey, AggMetric metric, AggIntervalEntry interval);

2
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java

@ -23,7 +23,7 @@ import java.util.List;
public interface CalculatedFieldResult { public interface CalculatedFieldResult {
TbMsg toTbMsg(EntityId entityId, List<CalculatedFieldId> cfIds); TbMsg toTbMsg(EntityId entityId, String cfName, List<CalculatedFieldId> cfIds);
String stringValue(); String stringValue();

45
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java

@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEn
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
@ -69,7 +68,6 @@ import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto;
@Slf4j @Slf4j
public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedFieldProcessingService implements CalculatedFieldProcessingService { public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedFieldProcessingService implements CalculatedFieldProcessingService {
private final TbClusterService clusterService;
private final PartitionService partitionService; private final PartitionService partitionService;
public DefaultCalculatedFieldProcessingService(AttributesService attributesService, public DefaultCalculatedFieldProcessingService(AttributesService attributesService,
@ -80,8 +78,7 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF
TbClusterService clusterService, TbClusterService clusterService,
TelemetrySubscriptionService tsSubService, TelemetrySubscriptionService tsSubService,
PartitionService partitionService) { PartitionService partitionService) {
super(attributesService, timeseriesService, tsSubService, apiLimitService, relationService, ownerService); super(attributesService, timeseriesService, tsSubService, apiLimitService, relationService, ownerService, clusterService);
this.clusterService = clusterService;
this.partitionService = partitionService; this.partitionService = partitionService;
} }
@ -136,40 +133,40 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF
return super.fetchMetricDuringInterval(tenantId, entityId, argKey, metric, interval); return super.fetchMetricDuringInterval(tenantId, entityId, argKey, metric, interval);
} }
public void processResult(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List<CalculatedFieldId> cfIds, TbCallback callback) { public void processResult(TenantId tenantId, EntityId entityId, String cfName, CalculatedFieldResult result, List<CalculatedFieldId> cfIds, TbCallback callback) {
if (result instanceof AlarmCalculatedFieldResult) { if (result instanceof AlarmCalculatedFieldResult) {
sendMsgToRuleEngine(tenantId, entityId, callback, result.toTbMsg(entityId, cfIds)); sendMsgToRuleEngine(tenantId, entityId, callback, result.toTbMsg(entityId, cfName, cfIds));
return; return;
} }
TelemetryCalculatedFieldResult telemetryResult = result instanceof TelemetryCalculatedFieldResult telemetryRes TelemetryCalculatedFieldResult telemetryResult = result instanceof TelemetryCalculatedFieldResult telemetryRes
? telemetryRes : ((PropagationCalculatedFieldResult) result).getResult(); ? telemetryRes : ((PropagationCalculatedFieldResult) result).getResult();
switch (telemetryResult.getOutputStrategy().getType()) { switch (telemetryResult.getOutputStrategy().getType()) {
case IMMEDIATE -> processImmediately(tenantId, entityId, result, cfIds, callback); case IMMEDIATE -> processImmediately(tenantId, entityId, cfName, result, cfIds, callback);
case RULE_CHAIN -> pushMsgToRuleEngine(tenantId, entityId, result, cfIds, callback); case RULE_CHAIN -> pushMsgToRuleEngine(tenantId, entityId, cfName, result, cfIds, callback);
} }
} }
private void processImmediately(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List<CalculatedFieldId> cfIds, TbCallback callback) { private void processImmediately(TenantId tenantId, EntityId entityId, String cfName, CalculatedFieldResult result, List<CalculatedFieldId> cfIds, TbCallback callback) {
if (result instanceof TelemetryCalculatedFieldResult telemetryResult) { if (result instanceof TelemetryCalculatedFieldResult telemetryResult) {
saveTelemetryResult(tenantId, entityId, telemetryResult, cfIds, callback); saveTelemetryResult(tenantId, entityId, cfName, telemetryResult, cfIds, callback);
return; return;
} }
if (result instanceof PropagationCalculatedFieldResult propagationResult) { if (result instanceof PropagationCalculatedFieldResult propagationResult) {
handlePropagationResults(propagationResult, callback, handlePropagationResults(propagationResult, callback,
(entity, res, cb) -> saveTelemetryResult(tenantId, entity, res, cfIds, cb)); (entity, res, cb) -> saveTelemetryResult(tenantId, entity, cfName, res, cfIds, cb));
return; return;
} }
callback.onSuccess(); callback.onSuccess();
} }
private void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List<CalculatedFieldId> cfIds, TbCallback callback) { private void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, String cfName, CalculatedFieldResult result, List<CalculatedFieldId> cfIds, TbCallback callback) {
if (result instanceof PropagationCalculatedFieldResult propagationResult) { if (result instanceof PropagationCalculatedFieldResult propagationResult) {
handlePropagationResults(propagationResult, callback, handlePropagationResults(propagationResult, callback,
(entity, res, cb) -> sendMsgToRuleEngine(tenantId, entity, cb, res.toTbMsg(entity, cfIds))); (entity, res, cb) -> sendMsgToRuleEngine(tenantId, entity, cb, res.toTbMsg(entity, cfName, cfIds)));
return; return;
} }
sendMsgToRuleEngine(tenantId, entityId, callback, result.toTbMsg(entityId, cfIds)); sendMsgToRuleEngine(tenantId, entityId, callback, result.toTbMsg(entityId, cfName, cfIds));
} }
private void handlePropagationResults(PropagationCalculatedFieldResult propagationResult, TbCallback callback, private void handlePropagationResults(PropagationCalculatedFieldResult propagationResult, TbCallback callback,
@ -190,26 +187,6 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF
} }
} }
private void sendMsgToRuleEngine(TenantId tenantId, EntityId entityId, TbCallback callback, TbMsg msg) {
try {
clusterService.pushMsgToRuleEngine(tenantId, entityId, msg, new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
log.trace("[{}][{}] Pushed message to rule engine: {} ", tenantId, entityId, msg);
callback.onSuccess();
}
@Override
public void onFailure(Throwable t) {
callback.onFailure(t);
}
});
} catch (Exception e) {
log.warn("[{}][{}] Failed to push message to rule engine: {}", tenantId, entityId, msg, e);
callback.onFailure(e);
}
}
@Override @Override
public void pushMsgToLinks(CalculatedFieldTelemetryMsg msg, List<CalculatedFieldEntityCtxId> linkedCalculatedFields, TbCallback callback) { public void pushMsgToLinks(CalculatedFieldTelemetryMsg msg, List<CalculatedFieldEntityCtxId> linkedCalculatedFields, TbCallback callback) {
Map<TopicPartitionInfo, List<CalculatedFieldEntityCtxId>> unicasts = new HashMap<>(); Map<TopicPartitionInfo, List<CalculatedFieldEntityCtxId>> unicasts = new HashMap<>();

4
application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java

@ -32,8 +32,8 @@ public final class PropagationCalculatedFieldResult implements CalculatedFieldRe
private final TelemetryCalculatedFieldResult result; private final TelemetryCalculatedFieldResult result;
@Override @Override
public TbMsg toTbMsg(EntityId entityId, List<CalculatedFieldId> cfIds) { public TbMsg toTbMsg(EntityId entityId, String cfName, List<CalculatedFieldId> cfIds) {
return result.toTbMsg(entityId, cfIds); return result.toTbMsg(entityId, cfName, cfIds);
} }
@Override @Override

13
application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java

@ -28,8 +28,8 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.List; import java.util.List;
import java.util.Map;
import static org.thingsboard.server.common.data.DataConstants.CF_NAME_METADATA_KEY;
import static org.thingsboard.server.common.data.DataConstants.SCOPE; import static org.thingsboard.server.common.data.DataConstants.SCOPE;
@Data @Data
@ -44,15 +44,16 @@ public final class TelemetryCalculatedFieldResult implements CalculatedFieldResu
public static final TelemetryCalculatedFieldResult EMPTY = TelemetryCalculatedFieldResult.builder().result(null).build(); public static final TelemetryCalculatedFieldResult EMPTY = TelemetryCalculatedFieldResult.builder().result(null).build();
@Override @Override
public TbMsg toTbMsg(EntityId entityId, List<CalculatedFieldId> cfIds) { public TbMsg toTbMsg(EntityId entityId, String cfName, List<CalculatedFieldId> cfIds) {
TbMsgType msgType = switch (type) { TbMsgType msgType = switch (type) {
case ATTRIBUTES -> TbMsgType.POST_ATTRIBUTES_REQUEST; case ATTRIBUTES -> TbMsgType.POST_ATTRIBUTES_REQUEST;
case TIME_SERIES -> TbMsgType.POST_TELEMETRY_REQUEST; case TIME_SERIES -> TbMsgType.POST_TELEMETRY_REQUEST;
}; };
TbMsgMetaData metaData = switch (type) { TbMsgMetaData metaData = new TbMsgMetaData();
case ATTRIBUTES -> new TbMsgMetaData(Map.of(SCOPE, scope.name())); metaData.putValue(CF_NAME_METADATA_KEY, cfName);
case TIME_SERIES -> TbMsgMetaData.EMPTY; if (OutputType.ATTRIBUTES == type) {
}; metaData.putValue(SCOPE, scope.name());
}
return TbMsg.newMsg() return TbMsg.newMsg()
.type(msgType) .type(msgType)
.originator(entityId) .originator(entityId)

8
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.cf.ctx.state; package org.thingsboard.server.service.cf.ctx.state;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@ -35,6 +36,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
@Getter @Getter
public abstract class BaseCalculatedFieldState implements CalculatedFieldState, Closeable { public abstract class BaseCalculatedFieldState implements CalculatedFieldState, Closeable {
@ -190,4 +192,10 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState,
return ReadinessStatus.from(emptyArguments); return ReadinessStatus.from(emptyArguments);
} }
@Override
public JsonNode getArgumentsJson() {
return JacksonUtil.valueToTree(arguments.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().jsonValue())));
}
} }

6
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -86,6 +86,7 @@ public class CalculatedFieldCtx implements Closeable {
private CalculatedField calculatedField; private CalculatedField calculatedField;
private CalculatedFieldId cfId; private CalculatedFieldId cfId;
private String cfName;
private TenantId tenantId; private TenantId tenantId;
private EntityId entityId; private EntityId entityId;
private CalculatedFieldType cfType; private CalculatedFieldType cfType;
@ -130,6 +131,7 @@ public class CalculatedFieldCtx implements Closeable {
this.calculatedField = calculatedField; this.calculatedField = calculatedField;
this.cfId = calculatedField.getId(); this.cfId = calculatedField.getId();
this.cfName = calculatedField.getName();
this.tenantId = calculatedField.getTenantId(); this.tenantId = calculatedField.getTenantId();
this.entityId = calculatedField.getEntityId(); this.entityId = calculatedField.getEntityId();
this.cfType = calculatedField.getType(); this.cfType = calculatedField.getType();
@ -646,7 +648,9 @@ public class CalculatedFieldCtx implements Closeable {
} }
if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig
&& other.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig && other.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig
&& (thisConfig.getDeduplicationIntervalInSec() != otherConfig.getDeduplicationIntervalInSec() || !thisConfig.getMetrics().equals(otherConfig.getMetrics()))) { && (thisConfig.getDeduplicationIntervalInSec() != otherConfig.getDeduplicationIntervalInSec()
|| !thisConfig.getMetrics().equals(otherConfig.getMetrics())
|| thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs())) {
return true; return true;
} }
if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig

3
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
@ -60,6 +61,8 @@ public interface CalculatedFieldState extends Closeable {
Map<String, ArgumentEntry> getArguments(); Map<String, ArgumentEntry> getArguments();
JsonNode getArgumentsJson();
long getLatestTimestamp(); long getLatestTimestamp();
void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx); void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx);

28
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.cf.ctx.state.aggregation; package org.thingsboard.server.service.cf.ctx.state.aggregation;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -23,6 +24,7 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput;
@ -31,9 +33,11 @@ import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInp
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric;
import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType;
import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.aggregation.function.AggEntry; import org.thingsboard.server.service.cf.ctx.state.aggregation.function.AggEntry;
@ -44,6 +48,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
@ -62,6 +67,8 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat
private ScheduledFuture<?> reevaluationFuture; private ScheduledFuture<?> reevaluationFuture;
private EntityService entityService;
public RelatedEntitiesAggregationCalculatedFieldState(EntityId entityId) { public RelatedEntitiesAggregationCalculatedFieldState(EntityId entityId) {
super(entityId); super(entityId);
} }
@ -72,6 +79,7 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat
var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
metrics = configuration.getMetrics(); metrics = configuration.getMetrics();
deduplicationIntervalMs = SECONDS.toMillis(configuration.getDeduplicationIntervalInSec()); deduplicationIntervalMs = SECONDS.toMillis(configuration.getDeduplicationIntervalInSec());
entityService = ctx.getSystemContext().getEntityService();
} }
@Override @Override
@ -247,4 +255,24 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat
} }
} }
@Override
public JsonNode getArgumentsJson() {
Map<EntityId, Map<String, ArgumentEntry>> inputs = prepareInputs();
Map<EntityId, EntityInfo> entityIdEntityInfos = entityService.fetchEntityInfos(ctx.getTenantId(), null, inputs.keySet());
List<EntityArgument> entitiesArguments = new ArrayList<>();
inputs.forEach((entityId, entityArguments) -> {
EntityInfo entityInfo = entityIdEntityInfos.get(entityId);
if (entityInfo != null) {
JsonNode entityArgumentsJson = JacksonUtil.valueToTree(entityArguments.entrySet().stream()
.collect(Collectors.toMap(Entry::getKey, e -> e.getValue().jsonValue())));
entitiesArguments.add(new EntityArgument(entityInfo, entityArgumentsJson));
}
});
return JacksonUtil.valueToTree(new RelatedEntitiesArgument(ArgumentEntryType.RELATED_ENTITIES, entitiesArguments));
}
record RelatedEntitiesArgument(ArgumentEntryType type, List<EntityArgument> entitiesArguments) {}
record EntityArgument(EntityInfo entity, JsonNode entityArguments) {}
} }

100
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java

@ -15,12 +15,16 @@
*/ */
package org.thingsboard.server.service.cf.ctx.state.aggregation.single; package org.thingsboard.server.service.cf.ctx.state.aggregation.single;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbUtils; import org.thingsboard.script.api.tbel.TbUtils;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
@ -36,6 +40,7 @@ import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
@ -58,6 +63,8 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
private long checkInterval; private long checkInterval;
private Map<String, AggMetric> metrics; private Map<String, AggMetric> metrics;
private EntityAggregationDebugArgumentsTracker debugTracker;
private CalculatedFieldProcessingService cfProcessingService; private CalculatedFieldProcessingService cfProcessingService;
public EntityAggregationCalculatedFieldState(EntityId entityId) { public EntityAggregationCalculatedFieldState(EntityId entityId) {
@ -94,6 +101,15 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
createIntervalIfNotExist(); createIntervalIfNotExist();
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) {
if (debugTracker == null) {
debugTracker = new EntityAggregationDebugArgumentsTracker(new HashMap<>());
} else {
debugTracker.reset();
}
debugTracker.recordUpdatedArgs(updatedArgs, arguments);
}
Map<AggIntervalEntry, Map<String, ArgumentEntry>> results = new HashMap<>(); Map<AggIntervalEntry, Map<String, ArgumentEntry>> results = new HashMap<>();
List<AggIntervalEntry> expiredIntervals = new ArrayList<>(); List<AggIntervalEntry> expiredIntervals = new ArrayList<>();
getIntervals().forEach((intervalEntry, argIntervalStatuses) -> { getIntervals().forEach((intervalEntry, argIntervalStatuses) -> {
@ -114,6 +130,12 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
.build()); .build());
} }
@Override
public Map<String, ArgumentEntry> update(Map<String, ArgumentEntry> argumentValues, CalculatedFieldCtx ctx) {
createIntervalIfNotExist();
return super.update(argumentValues, ctx);
}
private void removeExpiredIntervals(List<AggIntervalEntry> expiredIntervals) { private void removeExpiredIntervals(List<AggIntervalEntry> expiredIntervals) {
expiredIntervals.forEach(expiredInterval -> { expiredIntervals.forEach(expiredInterval -> {
arguments.values().stream() arguments.values().stream()
@ -183,6 +205,9 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
expiredIntervals.add(intervalEntry); expiredIntervals.add(intervalEntry);
} else if (now - startTs >= intervalEntry.getIntervalDuration()) { } else if (now - startTs >= intervalEntry.getIntervalDuration()) {
handleActiveInterval(intervalEntry, args, results); handleActiveInterval(intervalEntry, args, results);
if (watermarkDuration == 0) {
expiredIntervals.add(intervalEntry);
}
} }
} }
@ -262,14 +287,89 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
resultNode.put("ts", interval.getEndTs() - 1); resultNode.put("ts", interval.getEndTs() - 1);
resultNode.set("values", metricsNode); resultNode.set("values", metricsNode);
result.add(resultNode); result.add(resultNode);
if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) {
if (debugTracker != null) {
debugTracker.addInterval(interval);
}
}
} }
}); });
return result; return result;
} }
@Override
public JsonNode getArgumentsJson() {
if (debugTracker == null) {
return null;
}
EntityAggregationDebugArguments debugArguments = debugTracker.toDebugArguments();
return debugArguments == null ? null : JacksonUtil.valueToTree(debugArguments);
}
@Override @Override
public boolean isReady() { public boolean isReady() {
return true; return true;
} }
record EntityAggregationDebugArgumentsTracker(Map<AggIntervalEntry, Map<String, TbelCfArg>> processedIntervals) {
public void reset() {
processedIntervals.clear();
}
public void addInterval(AggIntervalEntry interval) {
processedIntervals.computeIfAbsent(interval, k -> new HashMap<>());
}
public void recordUpdatedArgs(Map<String, ArgumentEntry> updatedArgs, Map<String, ArgumentEntry> arguments) {
if (updatedArgs != null && !updatedArgs.isEmpty()) {
updatedArgs.forEach((argName, argEntry) -> {
ArgumentEntry argumentEntry = arguments.get(argName);
if (argumentEntry instanceof EntityAggregationArgumentEntry entityAggEntry && argEntry instanceof SingleValueArgumentEntry singleEntry) {
entityAggEntry.getAggIntervals().forEach((aggIntervalEntry, aggIntervalEntryStatus) -> {
boolean match = singleEntry.isForceResetPrevious() || aggIntervalEntry.belongsToInterval(singleEntry.getTs());
if (match) {
recordArg(aggIntervalEntry, argName, singleEntry.toTbelCfArg());
}
});
}
});
}
}
public void recordArg(AggIntervalEntry interval, String argName, TbelCfArg value) {
processedIntervals.computeIfAbsent(interval, k -> new HashMap<>()).put(argName, value);
}
public EntityAggregationDebugArguments toDebugArguments() {
if (processedIntervals.isEmpty()) {
return null;
}
return EntityAggregationDebugArguments.toDebugArguments(processedIntervals);
}
}
record EntityAggregationDebugArguments(List<IntervalDebugArgument> processedIntervals) {
public static EntityAggregationDebugArguments toDebugArguments(Map<AggIntervalEntry, Map<String, TbelCfArg>> processedIntervals) {
List<IntervalDebugArgument> result = new ArrayList<>();
processedIntervals.forEach((interval, args) -> {
result.add(new IntervalDebugArgument(interval.getStartTs(), interval.getEndTs(), args));
});
return new EntityAggregationDebugArguments(result);
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)
record IntervalDebugArgument(Long intervalStartTs, Long intervalEndTs, JsonNode updatedArguments) {
public IntervalDebugArgument(Long intervalStartTs, Long intervalEndTs, Map<String, TbelCfArg> updatedArguments) {
this(intervalStartTs, intervalEndTs, updatedArguments == null || updatedArguments.isEmpty() ? null : JacksonUtil.valueToTree(updatedArguments));
}
}
} }

5
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -75,6 +75,7 @@ import org.thingsboard.server.service.edge.rpc.processor.resource.ResourceEdgePr
import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainMetadataEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainMetadataEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.telemetry.TelemetryEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.telemetry.TelemetryEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.user.UserProcessor;
import org.thingsboard.server.service.edge.rpc.sync.EdgeRequestsService; import org.thingsboard.server.service.edge.rpc.sync.EdgeRequestsService;
import org.thingsboard.server.service.executors.GrpcCallbackExecutorService; import org.thingsboard.server.service.executors.GrpcCallbackExecutorService;
@ -265,9 +266,13 @@ public class EdgeContextComponent {
@Autowired @Autowired
private AiModelService aiModelService; private AiModelService aiModelService;
@Autowired @Autowired
private AiModelProcessor aiModelProcessor; private AiModelProcessor aiModelProcessor;
@Autowired
private UserProcessor userProcessor;
public EdgeProcessor getProcessor(EdgeEventType edgeEventType) { public EdgeProcessor getProcessor(EdgeEventType edgeEventType) {
EdgeProcessor processor = processorMap.get(edgeEventType); EdgeProcessor processor = processorMap.get(edgeEventType);
if (processor == null) { if (processor == null) {

24
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -84,6 +84,8 @@ import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg;
import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import org.thingsboard.server.gen.edge.v1.WidgetBundleTypesRequestMsg; import org.thingsboard.server.gen.edge.v1.WidgetBundleTypesRequestMsg;
import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.EdgeContextComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
@ -101,6 +103,7 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@ -122,6 +125,7 @@ public abstract class EdgeGrpcSession implements Closeable {
private final EdgeSessionState sessionState = new EdgeSessionState(); private final EdgeSessionState sessionState = new EdgeSessionState();
private final ReentrantLock downlinkMsgLock = new ReentrantLock(); private final ReentrantLock downlinkMsgLock = new ReentrantLock();
private final Lock sequenceDependencyLock = new ReentrantLock();
protected EdgeContextComponent ctx; protected EdgeContextComponent ctx;
protected Edge edge; protected Edge edge;
@ -940,6 +944,26 @@ public abstract class EdgeGrpcSession implements Closeable {
result.add(ctx.getAiModelProcessor().processAiModelMsgFromEdge(edge.getTenantId(), edge, aiModelUpdateMsg)); result.add(ctx.getAiModelProcessor().processAiModelMsgFromEdge(edge.getTenantId(), edge, aiModelUpdateMsg));
} }
} }
if (uplinkMsg.getUserUpdateMsgCount() > 0) {
for (UserUpdateMsg userUpdateMsg : uplinkMsg.getUserUpdateMsgList()) {
sequenceDependencyLock.lock();
try {
result.add(ctx.getUserProcessor().processUserMsgFromEdge(edge.getTenantId(), edge, userUpdateMsg));
} finally {
sequenceDependencyLock.unlock();
}
}
}
if (uplinkMsg.getUserCredentialsUpdateMsgCount() > 0) {
for (UserCredentialsUpdateMsg userCredentialsUpdateMsg : uplinkMsg.getUserCredentialsUpdateMsgList()) {
sequenceDependencyLock.lock();
try {
result.add(ctx.getUserProcessor().processUserCredentialsMsgFromEdge(edge.getTenantId(), edge, userCredentialsUpdateMsg));
} finally {
sequenceDependencyLock.unlock();
}
}
}
} catch (Exception e) { } catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e); log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e);

26
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java

@ -19,12 +19,14 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType;
@ -37,6 +39,7 @@ import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
@ -348,6 +351,29 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
edgeCtx.getRelationService().saveRelation(tenantId, relation); edgeCtx.getRelationService().saveRelation(tenantId, relation);
} }
protected <T extends HasId<? extends EntityId>> void pushEntityEventToRuleEngine(TenantId tenantId, T entity, TbMsgType msgType) {
pushEntityEventToRuleEngine(tenantId, null, entity, msgType);
}
protected <T extends HasId<? extends EntityId>> void pushEntityEventToRuleEngine(TenantId tenantId, Edge edge, T entity, TbMsgType msgType) {
try {
String entityAsString = JacksonUtil.toString(entity);
CustomerId customerId = getCustomerId(entity);
TbMsgMetaData tbMsgMetaData = edge == null ? TbMsgMetaData.EMPTY : getEdgeActionTbMsgMetaData(edge, customerId);
pushEntityEventToRuleEngine(tenantId, entity.getId(), customerId, msgType, entityAsString, tbMsgMetaData);
} catch (Exception e) {
log.warn("[{}][{}] Failed to push entity action for {} to rule engine: {}", tenantId, entity.getId(), entity.getId().getEntityType(), msgType.name(), e);
}
}
private <T extends HasId<? extends EntityId>> CustomerId getCustomerId(T entity) {
if (entity instanceof HasCustomerId hasCustomer) {
return hasCustomer.getCustomerId();
}
return null;
}
protected TbMsgMetaData getEdgeActionTbMsgMetaData(Edge edge, CustomerId customerId) { protected TbMsgMetaData getEdgeActionTbMsgMetaData(Edge edge, CustomerId customerId) {
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("edgeId", edge.getId().toString()); metaData.putValue("edgeId", edge.getId().toString());

34
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java

@ -55,21 +55,17 @@ public class AssetEdgeProcessor extends BaseAssetProcessor implements AssetProce
try { try {
edgeSynchronizationManager.getEdgeId().set(edge.getId()); edgeSynchronizationManager.getEdgeId().set(edge.getId());
switch (assetUpdateMsg.getMsgType()) { return switch (assetUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_CREATED_RPC_MESSAGE, ENTITY_UPDATED_RPC_MESSAGE -> {
case ENTITY_UPDATED_RPC_MESSAGE:
saveOrUpdateAsset(tenantId, assetId, assetUpdateMsg, edge); saveOrUpdateAsset(tenantId, assetId, assetUpdateMsg, edge);
return Futures.immediateFuture(null); yield Futures.immediateFuture(null);
case ENTITY_DELETED_RPC_MESSAGE: }
Asset assetToDelete = edgeCtx.getAssetService().findAssetById(tenantId, assetId); case ENTITY_DELETED_RPC_MESSAGE -> {
if (assetToDelete != null) { deleteAsset(tenantId, edge, assetId);
edgeCtx.getAssetService().unassignAssetFromEdge(tenantId, assetId, edge.getId()); yield Futures.immediateFuture(null);
} }
return Futures.immediateFuture(null); default -> handleUnsupportedMsgType(assetUpdateMsg.getMsgType());
case UNRECOGNIZED: };
default:
return handleUnsupportedMsgType(assetUpdateMsg.getMsgType());
}
} catch (DataValidationException e) { } catch (DataValidationException e) {
if (e.getMessage().contains("limit reached")) { if (e.getMessage().contains("limit reached")) {
log.warn("[{}] Number of allowed asset violated {}", tenantId, assetUpdateMsg, e); log.warn("[{}] Number of allowed asset violated {}", tenantId, assetUpdateMsg, e);
@ -97,14 +93,8 @@ public class AssetEdgeProcessor extends BaseAssetProcessor implements AssetProce
} }
private void pushAssetCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AssetId assetId) { private void pushAssetCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AssetId assetId) {
try { Asset asset = edgeCtx.getAssetService().findAssetById(tenantId, assetId);
Asset asset = edgeCtx.getAssetService().findAssetById(tenantId, assetId); pushEntityEventToRuleEngine(tenantId, edge, asset, TbMsgType.ENTITY_CREATED);
String assetAsString = JacksonUtil.toString(asset);
TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, asset.getCustomerId());
pushEntityEventToRuleEngine(tenantId, assetId, asset.getCustomerId(), TbMsgType.ENTITY_CREATED, assetAsString, msgMetaData);
} catch (Exception e) {
log.warn("[{}][{}] Failed to push asset action to rule engine: {}", tenantId, assetId, TbMsgType.ENTITY_CREATED.name(), e);
}
} }
@Override @Override

14
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java

@ -21,9 +21,11 @@ import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@ -77,4 +79,16 @@ public abstract class BaseAssetProcessor extends BaseEdgeProcessor {
protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, Asset asset, AssetUpdateMsg assetUpdateMsg); protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, Asset asset, AssetUpdateMsg assetUpdateMsg);
protected void deleteAsset(TenantId tenantId, AssetId assetId) {
deleteAsset(tenantId, null, assetId);
}
protected void deleteAsset(TenantId tenantId, Edge edge, AssetId assetId) {
Asset assetById = edgeCtx.getAssetService().findAssetById(tenantId, assetId);
if (assetById != null) {
edgeCtx.getAssetService().deleteAsset(tenantId, assetId);
pushEntityEventToRuleEngine(tenantId, edge, assetById, TbMsgType.ENTITY_DELETED);
}
}
} }

14
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java

@ -20,9 +20,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@ -100,6 +102,18 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor {
} }
} }
protected void deleteDashboard(TenantId tenantId, DashboardId dashboardId) {
deleteDashboard(tenantId, null, dashboardId);
}
protected void deleteDashboard(TenantId tenantId, Edge edge, DashboardId dashboardId) {
Dashboard dashboardById = edgeCtx.getDashboardService().findDashboardById(tenantId, dashboardId);
if (dashboardById != null) {
edgeCtx.getDashboardService().deleteDashboard(tenantId, dashboardId);
pushEntityEventToRuleEngine(tenantId, edge, dashboardById, TbMsgType.ENTITY_DELETED);
}
}
protected abstract Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers); protected abstract Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers);
} }

17
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java

@ -19,7 +19,6 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.ShortCustomerInfo;
@ -29,7 +28,6 @@ import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
@ -59,10 +57,7 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor implements Da
saveOrUpdateDashboard(tenantId, dashboardId, dashboardUpdateMsg, edge); saveOrUpdateDashboard(tenantId, dashboardId, dashboardUpdateMsg, edge);
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
case ENTITY_DELETED_RPC_MESSAGE: case ENTITY_DELETED_RPC_MESSAGE:
Dashboard dashboardToDelete = edgeCtx.getDashboardService().findDashboardById(tenantId, dashboardId); deleteDashboard(tenantId, edge, dashboardId);
if (dashboardToDelete != null) {
edgeCtx.getDashboardService().unassignDashboardFromEdge(tenantId, dashboardId, edge.getId());
}
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
case UNRECOGNIZED: case UNRECOGNIZED:
default: default:
@ -90,14 +85,8 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor implements Da
} }
private void pushDashboardCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DashboardId dashboardId) { private void pushDashboardCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DashboardId dashboardId) {
try { Dashboard dashboard = edgeCtx.getDashboardService().findDashboardById(tenantId, dashboardId);
Dashboard dashboard = edgeCtx.getDashboardService().findDashboardById(tenantId, dashboardId); pushEntityEventToRuleEngine(tenantId, edge, dashboard, TbMsgType.ENTITY_CREATED);
String dashboardAsString = JacksonUtil.toString(dashboard);
TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, null);
pushEntityEventToRuleEngine(tenantId, dashboardId, null, TbMsgType.ENTITY_CREATED, dashboardAsString, msgMetaData);
} catch (Exception e) {
log.warn("[{}][{}] Failed to push dashboard action to rule engine: {}", tenantId, dashboardId, TbMsgType.ENTITY_CREATED.name(), e);
}
} }
@Override @Override

13
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java

@ -21,9 +21,11 @@ import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
@ -110,4 +112,15 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor {
protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, Device device, DeviceUpdateMsg deviceUpdateMsg); protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, Device device, DeviceUpdateMsg deviceUpdateMsg);
protected void deleteDevice(TenantId tenantId, DeviceId deviceId) {
deleteDevice(tenantId, null, deviceId);
}
protected void deleteDevice(TenantId tenantId, Edge edge, DeviceId deviceId) {
Device deviceById = edgeCtx.getDeviceService().findDeviceById(tenantId, deviceId);
if (deviceById != null) {
edgeCtx.getDeviceService().deleteDevice(tenantId, deviceId);
pushEntityEventToRuleEngine(tenantId, edge, deviceById, TbMsgType.ENTITY_DELETED);
}
}
} }

15
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java

@ -76,10 +76,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr
saveOrUpdateDevice(tenantId, deviceId, deviceUpdateMsg, edge); saveOrUpdateDevice(tenantId, deviceId, deviceUpdateMsg, edge);
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
case ENTITY_DELETED_RPC_MESSAGE: case ENTITY_DELETED_RPC_MESSAGE:
Device deviceToDelete = edgeCtx.getDeviceService().findDeviceById(tenantId, deviceId); deleteDevice(tenantId, edge, deviceId);
if (deviceToDelete != null) {
edgeCtx.getDeviceService().unassignDeviceFromEdge(tenantId, deviceId, edge.getId());
}
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
case UNRECOGNIZED: case UNRECOGNIZED:
default: default:
@ -125,14 +122,8 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr
} }
private void pushDeviceCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DeviceId deviceId) { private void pushDeviceCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DeviceId deviceId) {
try { Device device = edgeCtx.getDeviceService().findDeviceById(tenantId, deviceId);
Device device = edgeCtx.getDeviceService().findDeviceById(tenantId, deviceId); pushEntityEventToRuleEngine(tenantId, edge, device, TbMsgType.ENTITY_CREATED);
String deviceAsString = JacksonUtil.toString(device);
TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, device.getCustomerId());
pushEntityEventToRuleEngine(tenantId, deviceId, device.getCustomerId(), TbMsgType.ENTITY_CREATED, deviceAsString, msgMetaData);
} catch (Exception e) {
log.warn("[{}][{}] Failed to push device action to rule engine: {}", tenantId, deviceId, TbMsgType.ENTITY_CREATED.name(), e);
}
} }
@Override @Override

13
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java

@ -21,9 +21,11 @@ import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@ -69,4 +71,15 @@ public abstract class BaseEntityViewProcessor extends BaseEdgeProcessor {
protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, EntityView entityView, EntityViewUpdateMsg entityViewUpdateMsg); protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, EntityView entityView, EntityViewUpdateMsg entityViewUpdateMsg);
protected void deleteEntityView(TenantId tenantId, EntityViewId entityViewId) {
deleteEntityView(tenantId, null, entityViewId);
}
protected void deleteEntityView(TenantId tenantId, Edge edge, EntityViewId entityViewId) {
EntityView entityViewById = edgeCtx.getEntityViewService().findEntityViewById(tenantId, entityViewId);
if (entityViewById != null) {
edgeCtx.getEntityViewService().deleteEntityView(tenantId, entityViewId);
pushEntityEventToRuleEngine(tenantId, edge, entityViewById, TbMsgType.ENTITY_DELETED);
}
}
} }

34
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java

@ -54,21 +54,17 @@ public class EntityViewEdgeProcessor extends BaseEntityViewProcessor implements
try { try {
edgeSynchronizationManager.getEdgeId().set(edge.getId()); edgeSynchronizationManager.getEdgeId().set(edge.getId());
switch (entityViewUpdateMsg.getMsgType()) { return switch (entityViewUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_CREATED_RPC_MESSAGE, ENTITY_UPDATED_RPC_MESSAGE -> {
case ENTITY_UPDATED_RPC_MESSAGE:
saveOrUpdateEntityView(tenantId, entityViewId, entityViewUpdateMsg, edge); saveOrUpdateEntityView(tenantId, entityViewId, entityViewUpdateMsg, edge);
return Futures.immediateFuture(null); yield Futures.immediateFuture(null);
case ENTITY_DELETED_RPC_MESSAGE: }
EntityView entityViewToDelete = edgeCtx.getEntityViewService().findEntityViewById(tenantId, entityViewId); case ENTITY_DELETED_RPC_MESSAGE -> {
if (entityViewToDelete != null) { deleteEntityView(tenantId, edge, entityViewId);
edgeCtx.getEntityViewService().unassignEntityViewFromEdge(tenantId, entityViewId, edge.getId()); yield Futures.immediateFuture(null);
} }
return Futures.immediateFuture(null); default -> handleUnsupportedMsgType(entityViewUpdateMsg.getMsgType());
case UNRECOGNIZED: };
default:
return handleUnsupportedMsgType(entityViewUpdateMsg.getMsgType());
}
} catch (DataValidationException e) { } catch (DataValidationException e) {
if (e.getMessage().contains("limit reached")) { if (e.getMessage().contains("limit reached")) {
log.warn("[{}] Number of allowed entity views violated {}", tenantId, entityViewUpdateMsg, e); log.warn("[{}] Number of allowed entity views violated {}", tenantId, entityViewUpdateMsg, e);
@ -96,14 +92,8 @@ public class EntityViewEdgeProcessor extends BaseEntityViewProcessor implements
} }
private void pushEntityViewCreatedEventToRuleEngine(TenantId tenantId, Edge edge, EntityViewId entityViewId) { private void pushEntityViewCreatedEventToRuleEngine(TenantId tenantId, Edge edge, EntityViewId entityViewId) {
try { EntityView entityView = edgeCtx.getEntityViewService().findEntityViewById(tenantId, entityViewId);
EntityView entityView = edgeCtx.getEntityViewService().findEntityViewById(tenantId, entityViewId); pushEntityEventToRuleEngine(tenantId, edge, entityView, TbMsgType.ENTITY_CREATED);
String entityViewAsString = JacksonUtil.toString(entityView);
TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, entityView.getCustomerId());
pushEntityEventToRuleEngine(tenantId, entityViewId, entityView.getCustomerId(), TbMsgType.ENTITY_CREATED, entityViewAsString, msgMetaData);
} catch (Exception e) {
log.warn("[{}][{}] Failed to push entity view action to rule engine: {}", tenantId, entityViewId, TbMsgType.ENTITY_CREATED.name(), e);
}
} }
@Override @Override

155
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java

@ -0,0 +1,155 @@
/**
* Copyright © 2016-2025 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.edge.rpc.processor.user;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.util.Pair;
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.edge.Edge;
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;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@Slf4j
public abstract class BaseUserProcessor extends BaseEdgeProcessor {
@Autowired
private DataValidator<User> userValidator;
protected Pair<Boolean, Boolean> saveOrUpdateUser(TenantId tenantId, UserId userId, UserUpdateMsg userUpdateMsg) {
boolean isCreated = false;
boolean userEmailUpdated = false;
try {
User user = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true);
if (user == null) {
throw new IllegalArgumentException(String.format("[%s] Failed to parse User from UserUpdateMsg: %s", tenantId, userUpdateMsg));
}
User userById = edgeCtx.getUserService().findUserById(tenantId, userId);
if (userById == null) {
isCreated = true;
user.setId(null);
} else {
user.setId(userId);
}
String userEmail = user.getEmail();
User existing = edgeCtx.getUserService().findUserByTenantIdAndEmail(tenantId, user.getEmail());
if (existing != null && !existing.getId().equals(user.getId())) {
String[] splitEmail = userEmail.split("@");
userEmail = splitEmail[0] + "_" + StringUtils.randomAlphanumeric(15) + "@" + splitEmail[1];
log.warn("[{}] User with email {} already exists. Renaming User email to {}",
tenantId, user.getEmail(), userEmail);
userEmailUpdated = true;
}
user.setEmail(userEmail);
setCustomerId(tenantId, isCreated ? null : userById.getCustomerId(), user, userUpdateMsg);
userValidator.validate(user, User::getTenantId);
if (isCreated) {
user.setId(userId);
}
edgeCtx.getUserService().saveUser(tenantId, user, false);
} catch (Exception e) {
log.error("[{}] Failed to process user update msg [{}]", tenantId, userUpdateMsg, e);
throw e;
}
return Pair.of(isCreated, userEmailUpdated);
}
protected void deleteUserAndPushEntityDeletedEventToRuleEngine(TenantId tenantId, UserId userId) {
deleteUserAndPushEntityDeletedEventToRuleEngine(tenantId, userId, null);
}
protected void deleteUserAndPushEntityDeletedEventToRuleEngine(TenantId tenantId, UserId userId, Edge edge) {
User removedUser = deleteUser(tenantId, userId);
if (removedUser == null) {
return;
}
CustomerId userCustomerId = removedUser.getCustomerId();
String userAsString = JacksonUtil.toString(removedUser);
TbMsgMetaData msgMetaData = edge == null ? new TbMsgMetaData() : getEdgeActionTbMsgMetaData(edge, userCustomerId);
addRemovedUserMetadata(msgMetaData, removedUser);
pushEntityEventToRuleEngine(tenantId, userId, userCustomerId, TbMsgType.ENTITY_DELETED, userAsString, msgMetaData);
}
private User deleteUser(TenantId tenantId, UserId userId) {
User userById = edgeCtx.getUserService().findUserById(tenantId, userId);
if (userById == null) {
log.trace("[{}] User with id {} does not exist", tenantId, userId);
return null;
}
edgeCtx.getUserService().deleteUser(tenantId, userById);
return userById;
}
protected void updateUserCredentials(TenantId tenantId, UserCredentialsUpdateMsg updateMsg) {
UserCredentials userCredentials = JacksonUtil.fromString(updateMsg.getEntity(), UserCredentials.class, true);
if (userCredentials == null) {
throw new IllegalArgumentException(String.format("[%s] Failed to parse UserCredentials from updateMsg: %s", tenantId, updateMsg));
}
User user = edgeCtx.getUserService().findUserById(tenantId, userCredentials.getUserId());
if (user == null) {
log.warn("[{}] Can't find user by id [{}] skipping credentials update. UserCredentialsUpdateMsg [{}]",
tenantId, userCredentials.getUserId(), updateMsg);
return;
}
log.debug("[{}] Updating user credentials for user [{}]. New credentials Id [{}], enabled [{}]",
tenantId, user.getName(), userCredentials.getId(), userCredentials.isEnabled());
try {
UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(tenantId, user.getId());
if (userCredentialsByUserId != null && !userCredentialsByUserId.getId().equals(userCredentials.getId())) {
edgeCtx.getUserService().deleteUserCredentials(tenantId, userCredentialsByUserId);
}
edgeCtx.getUserService().saveUserCredentials(tenantId, userCredentials, false);
} catch (Exception e) {
log.error("[{}] Can't update user credentials for user [{}], userCredentialsUpdateMsg [{}]",
tenantId, user.getName(), updateMsg, e);
throw new RuntimeException(e);
}
}
private void addRemovedUserMetadata(TbMsgMetaData metaData, User removedUser) {
metaData.putValue("userId", removedUser.getId().toString());
metaData.putValue("userName", removedUser.getName());
metaData.putValue("userEmail", removedUser.getEmail());
if (removedUser.getFirstName() != null) {
metaData.putValue("userFirstName", removedUser.getFirstName());
}
if (removedUser.getLastName() != null) {
metaData.putValue("userLastName", removedUser.getLastName());
}
}
protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, User user, UserUpdateMsg userUpdateMsg);
}

101
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java

@ -15,26 +15,110 @@
*/ */
package org.thingsboard.server.service.edge.rpc.processor.user; package org.thingsboard.server.service.edge.rpc.processor.user;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.util.UUID;
@Slf4j @Slf4j
@Component @Component
@TbCoreComponent @TbCoreComponent
public class UserEdgeProcessor extends BaseEdgeProcessor { public class UserEdgeProcessor extends BaseUserProcessor implements UserProcessor {
@Override
public ListenableFuture<Void> processUserMsgFromEdge(TenantId tenantId, Edge edge, UserUpdateMsg userUpdateMsg) {
log.trace("[{}] executing processUserMsgFromEdge [{}] from edge [{}]", tenantId, userUpdateMsg, edge.getId());
UserId userId = new UserId(new UUID(userUpdateMsg.getIdMSB(), userUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getEdgeId().set(edge.getId());
return switch (userUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE, ENTITY_UPDATED_RPC_MESSAGE -> {
saveOrUpdateUser(tenantId, userId, userUpdateMsg, edge);
yield Futures.immediateFuture(null);
}
case ENTITY_DELETED_RPC_MESSAGE -> {
deleteUserAndPushEntityDeletedEventToRuleEngine(tenantId, userId, edge);
yield Futures.immediateFuture(null);
}
default -> handleUnsupportedMsgType(userUpdateMsg.getMsgType());
};
} catch (DataValidationException e) {
if (e.getMessage().contains("limit reached")) {
log.warn("[{}] Number of allowed users violated {}", tenantId, userUpdateMsg, e);
return Futures.immediateFuture(null);
} else {
return Futures.immediateFailedFuture(e);
}
} finally {
edgeSynchronizationManager.getEdgeId().remove();
}
}
@Override
public ListenableFuture<Void> processUserCredentialsMsgFromEdge(TenantId tenantId, Edge edge, UserCredentialsUpdateMsg userCredentialsUpdateMsg) {
log.debug("[{}] Executing processUserCredentialsMsgFromEdge, userCredentialsUpdateMsg [{}]", tenantId, userCredentialsUpdateMsg);
try {
edgeSynchronizationManager.getEdgeId().set(edge.getId());
super.updateUserCredentials(tenantId, userCredentialsUpdateMsg);
} finally {
edgeSynchronizationManager.getEdgeId().remove();
}
return Futures.immediateFuture(null);
}
private void saveOrUpdateUser(TenantId tenantId, UserId userId, UserUpdateMsg userUpdateMsg, Edge edge) {
Pair<Boolean, Boolean> resultPair = super.saveOrUpdateUser(tenantId, userId, userUpdateMsg);
boolean isCreated = resultPair.getFirst();
if (isCreated) {
createRelationFromEdge(tenantId, edge.getId(), userId);
pushUserCreatedEventToRuleEngine(tenantId, edge, userId);
}
boolean userEmailUpdated = resultPair.getSecond();
if (userEmailUpdated) {
saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.USER, EdgeEventActionType.UPDATED, userId, null);
}
}
private void pushUserCreatedEventToRuleEngine(TenantId tenantId, Edge edge, UserId userId) {
try {
User user = edgeCtx.getUserService().findUserById(tenantId, userId);
if (user != null) {
String userAsString = JacksonUtil.toString(user);
TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, user.getCustomerId());
pushEntityEventToRuleEngine(tenantId, userId, user.getCustomerId(), TbMsgType.ENTITY_CREATED, userAsString, msgMetaData);
}
} catch (Exception e) {
log.warn("[{}][{}] Failed to push user action to rule engine: {}", tenantId, userId, TbMsgType.ENTITY_CREATED.name(), e);
}
}
@Override @Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
@ -48,7 +132,7 @@ public class UserEdgeProcessor extends BaseEdgeProcessor {
.setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addUserUpdateMsg(EdgeMsgConstructorUtils.constructUserUpdatedMsg(msgType, user)); .addUserUpdateMsg(EdgeMsgConstructorUtils.constructUserUpdatedMsg(msgType, user));
UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(edgeEvent.getTenantId(), userId);
if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { if (userCredentialsByUserId != null) {
builder.addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId)); builder.addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId));
} }
return builder.build(); return builder.build();
@ -62,11 +146,10 @@ public class UserEdgeProcessor extends BaseEdgeProcessor {
} }
case CREDENTIALS_UPDATED -> { case CREDENTIALS_UPDATED -> {
UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(edgeEvent.getTenantId(), userId);
if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { if (userCredentialsByUserId != null) {
UserCredentialsUpdateMsg userCredentialsUpdateMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId);
return DownlinkMsg.newBuilder() return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addUserCredentialsUpdateMsg(userCredentialsUpdateMsg) .addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId))
.build(); .build();
} }
} }
@ -79,4 +162,10 @@ public class UserEdgeProcessor extends BaseEdgeProcessor {
return EdgeEventType.USER; return EdgeEventType.USER;
} }
@Override
protected void setCustomerId(TenantId tenantId, CustomerId customerId, User user, UserUpdateMsg userUpdateMsg) {
CustomerId customerUUID = user.getCustomerId() != null ? user.getCustomerId() : customerId;
user.setCustomerId(customerUUID);
}
} }

31
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserProcessor.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2025 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.edge.rpc.processor.user;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
public interface UserProcessor extends EdgeProcessor {
ListenableFuture<Void> processUserMsgFromEdge(TenantId tenantId, Edge edge, UserUpdateMsg userUpdateMsg);
ListenableFuture<Void> processUserCredentialsMsgFromEdge(TenantId tenantId, Edge edge, UserCredentialsUpdateMsg userCredentialsUpdateMsg);
}

13
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java

@ -627,14 +627,11 @@ public class DefaultTbClusterService implements TbClusterService {
// No need to push notifications twice // No need to push notifications twice
tbRuleEngineServices.removeAll(tbCoreServices); tbRuleEngineServices.removeAll(tbCoreServices);
} }
boolean toRuleEngine = entityType != EntityType.USER; for (String serviceId : tbRuleEngineServices) {
if (toRuleEngine) { TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceId);
for (String serviceId : tbRuleEngineServices) { ToRuleEngineNotificationMsg toRuleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().setComponentLifecycle(componentLifecycleMsgProto).build();
TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceId); toRuleEngineProducer.send(tpi, new TbProtoQueueMsg<>(msg.getEntityId().getId(), toRuleEngineMsg), null);
ToRuleEngineNotificationMsg toRuleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().setComponentLifecycle(componentLifecycleMsgProto).build(); toRuleEngineNfs.incrementAndGet();
toRuleEngineProducer.send(tpi, new TbProtoQueueMsg<>(msg.getEntityId().getId(), toRuleEngineMsg), null);
toRuleEngineNfs.incrementAndGet();
}
} }
} }

2
application/src/main/java/org/thingsboard/server/service/user/cache/DefaultUserAuthDetailsCache.java

@ -29,13 +29,11 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
@Service @Service
@TbCoreComponent
@RequiredArgsConstructor @RequiredArgsConstructor
public class DefaultUserAuthDetailsCache implements UserAuthDetailsCache { public class DefaultUserAuthDetailsCache implements UserAuthDetailsCache {

16
application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.edge; package org.thingsboard.server.edge;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.protobuf.AbstractMessage; import com.google.protobuf.AbstractMessage;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
@ -28,8 +27,6 @@ import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
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.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
@ -37,10 +34,11 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest @DaoSqlTest
@ -277,12 +275,10 @@ public class AssetEdgeTest extends AbstractEdgeTest {
edgeImitator.expectResponsesAmount(1); edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertTrue(edgeImitator.waitForResponses());
AssetInfo assetInfo = doGet("/api/asset/info/" + savedAsset.getUuidId(), AssetInfo.class);
Assert.assertNotNull(assetInfo); await().atMost(30, TimeUnit.SECONDS).untilAsserted(() ->
List<AssetInfo> edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/assets?", doGet("/api/asset/info/" + savedAsset.getUuidId(), AssetInfo.class, status().isNotFound())
new TypeReference<PageData<AssetInfo>>() { );
}, new PageLink(100)).getData();
Assert.assertFalse(edgeAssets.contains(assetInfo));
} }
private Asset saveAssetOnCloudAndVerifyDeliveryToEdge() throws Exception { private Asset saveAssetOnCloudAndVerifyDeliveryToEdge() throws Exception {

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

@ -36,10 +36,11 @@ import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest @DaoSqlTest
@ -227,7 +228,7 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
} }
@Test @Test
public void testSendDeleteEntityViewOnEdgeToCloud() throws Exception { public void testSendDeleteDashboardOnEdgeToCloud() throws Exception {
Dashboard savedDashboard = saveDashboardOnCloudAndVerifyDeliveryToEdge(); Dashboard savedDashboard = saveDashboardOnCloudAndVerifyDeliveryToEdge();
UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder();
@ -244,12 +245,10 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
edgeImitator.expectResponsesAmount(1); edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertTrue(edgeImitator.waitForResponses());
DashboardInfo dashboardInfo = doGet("/api/dashboard/info/" + savedDashboard.getUuidId(), DashboardInfo.class);
Assert.assertNotNull(dashboardInfo); await().atMost(30, TimeUnit.SECONDS).untilAsserted(() ->
List<DashboardInfo> edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/dashboards?", doGet("/api/dashboard/info/" + savedDashboard.getUuidId(), DashboardInfo.class, status().isNotFound())
new TypeReference<PageData<DashboardInfo>>() { );
}, new PageLink(100)).getData();
Assert.assertFalse(edgeAssets.contains(dashboardInfo));
} }
private Dashboard saveDashboardOnCloudAndVerifyDeliveryToEdge() throws Exception { private Dashboard saveDashboardOnCloudAndVerifyDeliveryToEdge() throws Exception {
@ -263,10 +262,10 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
Optional<DashboardUpdateMsg> dashboardUpdateMsgOpt = edgeImitator.findMessageByType(DashboardUpdateMsg.class); Optional<DashboardUpdateMsg> dashboardUpdateMsgOpt = edgeImitator.findMessageByType(DashboardUpdateMsg.class);
Assert.assertTrue(dashboardUpdateMsgOpt.isPresent()); Assert.assertTrue(dashboardUpdateMsgOpt.isPresent());
DashboardUpdateMsg entityViewUpdateMsg = dashboardUpdateMsgOpt.get(); DashboardUpdateMsg dashboardUpdateMsg = dashboardUpdateMsgOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType());
Assert.assertEquals(savedDashboard.getUuidId().getMostSignificantBits(), entityViewUpdateMsg.getIdMSB()); Assert.assertEquals(savedDashboard.getUuidId().getMostSignificantBits(), dashboardUpdateMsg.getIdMSB());
Assert.assertEquals(savedDashboard.getUuidId().getLeastSignificantBits(), entityViewUpdateMsg.getIdLSB()); Assert.assertEquals(savedDashboard.getUuidId().getLeastSignificantBits(), dashboardUpdateMsg.getIdLSB());
return savedDashboard; return savedDashboard;
} }

11
application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java

@ -79,6 +79,7 @@ import java.util.Random;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@ -413,12 +414,10 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
edgeImitator.expectResponsesAmount(1); edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertTrue(edgeImitator.waitForResponses());
DeviceInfo deviceInfo = doGet("/api/device/info/" + savedDevice.getUuidId(), DeviceInfo.class);
Assert.assertNotNull(deviceInfo); await().atMost(30, TimeUnit.SECONDS).untilAsserted(() ->
List<DeviceInfo> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?", doGet("/api/device/info/" + savedDevice.getUuidId(), DeviceInfo.class, status().isNotFound())
new TypeReference<PageData<DeviceInfo>>() { );
}, new PageLink(100)).getData();
Assert.assertFalse(edgeDevices.contains(deviceInfo));
} }
@Test @Test

15
application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.edge; package org.thingsboard.server.edge;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.protobuf.AbstractMessage; import com.google.protobuf.AbstractMessage;
import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.Assert; import org.junit.Assert;
@ -31,8 +30,6 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg;
@ -40,10 +37,11 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest @DaoSqlTest
@ -256,12 +254,9 @@ public class EntityViewEdgeTest extends AbstractEdgeTest {
edgeImitator.expectResponsesAmount(1); edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertTrue(edgeImitator.waitForResponses());
EntityViewInfo entityViewInfo = doGet("/api/entityView/info/" + savedEntityView.getUuidId(), EntityViewInfo.class); await().atMost(30, TimeUnit.SECONDS).untilAsserted(() ->
Assert.assertNotNull(entityViewInfo); doGet("/api/entityView/info/" + savedEntityView.getUuidId(), EntityViewInfo.class, status().isNotFound())
List<EntityViewInfo> edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/entityViews?", );
new TypeReference<PageData<EntityViewInfo>>() {
}, new PageLink(100)).getData();
Assert.assertFalse(edgeAssets.contains(entityViewInfo));
} }
private void verifyEntityViewUpdateMsg(EntityView entityView, Device device) throws InvalidProtocolBufferException { private void verifyEntityViewUpdateMsg(EntityView entityView, Device device) throws InvalidProtocolBufferException {

393
application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java

@ -15,15 +15,22 @@
*/ */
package org.thingsboard.server.edge; package org.thingsboard.server.edge;
import com.google.protobuf.AbstractMessage; import com.fasterxml.jackson.databind.JsonNode;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.test.web.servlet.ResultMatcher;
import org.testcontainers.shaded.org.awaitility.Awaitility;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.UserCredentialsId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
@ -32,11 +39,15 @@ import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import org.thingsboard.server.service.security.model.ChangePasswordRequest; import org.thingsboard.server.service.security.model.ChangePasswordRequest;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.user.UserServiceImpl.DEFAULT_TOKEN_LENGTH;
@DaoSqlTest @DaoSqlTest
public class UserEdgeTest extends AbstractEdgeTest { public class UserEdgeTest extends AbstractEdgeTest {
@ -44,216 +55,304 @@ public class UserEdgeTest extends AbstractEdgeTest {
@Autowired @Autowired
private BCryptPasswordEncoder passwordEncoder; private BCryptPasswordEncoder passwordEncoder;
private static final String DEFAULT_FIRST_NAME = "Boris";
private static final String DEFAULT_LAST_NAME = "Johnson";
private static final String UPDATED_LAST_NAME = "Borisov";
private static final String DEFAULT_TENANT_ADMIN_EMAIL = "tenantAdmin@thingsboard.org";
private static final String DEFAULT_CUSTOMER_USER_EMAIL = "customerUser@thingsboard.org";
@Test @Test
public void testCreateUpdateDeleteTenantUser() throws Exception { public void testCreateUpdateDeleteTenantUser() throws Exception {
// create user // create user
edgeImitator.expectMessageAmount(3); User newTenantAdmin = buildUser(Authority.TENANT_ADMIN, null);
User newTenantAdmin = new User(); User savedTenantAdmin = createAndVerifyUserOnEdge(newTenantAdmin);
newTenantAdmin.setAuthority(Authority.TENANT_ADMIN);
newTenantAdmin.setTenantId(tenantId);
newTenantAdmin.setEmail("tenantAdmin@thingsboard.org");
newTenantAdmin.setFirstName("Boris");
newTenantAdmin.setLastName("Johnson");
User savedTenantAdmin = createUser(newTenantAdmin, "tenant");
Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user)
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size());
Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size());
Optional<UserUpdateMsg> userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(userUpdateMsgOpt.isPresent());
UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get();
User userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true);
Assert.assertNotNull(userMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType());
Assert.assertEquals(savedTenantAdmin.getId(), userMsg.getId());
Assert.assertEquals(savedTenantAdmin.getAuthority(), userMsg.getAuthority());
Assert.assertEquals(savedTenantAdmin.getEmail(), userMsg.getEmail());
Assert.assertEquals(savedTenantAdmin.getFirstName(), userMsg.getFirstName());
Assert.assertEquals(savedTenantAdmin.getLastName(), userMsg.getLastName());
Optional<UserCredentialsUpdateMsg> userCredentialsUpdateMsgOpt = edgeImitator.findMessageByType(UserCredentialsUpdateMsg.class);
Assert.assertTrue(userCredentialsUpdateMsgOpt.isPresent());
// update user // update user
edgeImitator.expectMessageAmount(2); updateAndVerifyUserLastName(savedTenantAdmin);
savedTenantAdmin.setLastName("Borisov");
savedTenantAdmin = doPost("/api/user", savedTenantAdmin, User.class);
Assert.assertTrue(edgeImitator.waitForMessages());
userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(userUpdateMsgOpt.isPresent());
userUpdateMsg = userUpdateMsgOpt.get();
userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true);
Assert.assertNotNull(userMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType());
Assert.assertEquals(savedTenantAdmin.getLastName(), userMsg.getLastName());
// update user credentials // update user credentials
login(savedTenantAdmin.getEmail(), "tenant"); login(savedTenantAdmin.getEmail(), "tenant");
updateAndVerifyUserCredentials(savedTenantAdmin);
loginTenantAdmin();
edgeImitator.expectMessageAmount(1); // delete user
ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); deleteAndVerifyUser(savedTenantAdmin);
changePasswordRequest.setCurrentPassword("tenant"); }
changePasswordRequest.setNewPassword("newTenant");
doPost("/api/auth/changePassword", changePasswordRequest); @Test
Assert.assertTrue(edgeImitator.waitForMessages()); public void testCreateUpdateDeleteCustomerUser() throws Exception {
AbstractMessage latestMessage = edgeImitator.getLatestMessage(); // create customer
Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); Customer savedCustomer = createAndAssignCustomerToEdge();
UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage;
UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true);
Assert.assertNotNull(userCredentialsMsg);
Assert.assertEquals(savedTenantAdmin.getId(), userCredentialsMsg.getUserId());
Assert.assertTrue(passwordEncoder.matches(changePasswordRequest.getNewPassword(), userCredentialsMsg.getPassword()));
// create user
User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId());
User savedCustomerUser = createAndVerifyUserOnEdge(customerUser);
// update user
updateAndVerifyUserLastName(savedCustomerUser);
// update user credentials
login(savedCustomerUser.getEmail(), "customer");
updateAndVerifyUserCredentials(savedCustomerUser);
loginTenantAdmin(); loginTenantAdmin();
// delete user // delete user
deleteAndVerifyUser(savedCustomerUser);
}
@Test
public void testSendUserToCloudFromEdge() throws Exception {
// create customer
Customer savedCustomer = createAndAssignCustomerToEdge();
// create uplinkMsg with user and userCredentials
UserId userId = new UserId(UUID.randomUUID());
UserCredentialsId userCredentialsId = new UserCredentialsId(UUID.randomUUID());
UplinkMsg uplinkMsg = buildUserUplinkMsg(userId, savedCustomer.getId(), userCredentialsId);
User userFromCloud = verifyMsgOnCloud(uplinkMsg, userId, false);
assertUserCredentialsFlags(userFromCloud, false, false);
// create uplinkMsg with enabled userCredentials
UplinkMsg uplinkMsgWithEnabledCredentials = constructUserCredentialsUplinkMsg(userCredentialsId, userId);
User cloudUserWithCredentials = verifyMsgOnCloud(uplinkMsgWithEnabledCredentials, userId, false);
assertUserCredentialsFlags(cloudUserWithCredentials, true, true);
// create uplinkMsg with user the same email
UserId secondUserId = new UserId(UUID.randomUUID());
UserCredentialsId secondCredentialsId = new UserCredentialsId(UUID.randomUUID());
UplinkMsg uplinkMsgForUserExistingEmail = buildUserUplinkMsg(secondUserId, savedCustomer.getId(), secondCredentialsId);
verifyMsgOnCloud(uplinkMsgForUserExistingEmail, secondUserId, true);
}
@Test
public void testSendUserCredentialsRequestToCloud() throws Exception {
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder();
userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits());
userCredentialsRequestMsgBuilder.setUserIdLSB(tenantAdminUserId.getId().getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(userCredentialsRequestMsgBuilder);
uplinkMsgBuilder.addUserCredentialsRequestMsg(userCredentialsRequestMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
doDelete("/api/user/" + savedTenantAdmin.getUuidId()) edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
.andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof UserUpdateMsg); UserCredentialsUpdateMsg userCredentialsUpdateMsg = getLatestUserCredentialsUpdateMsg();
userUpdateMsg = (UserUpdateMsg) latestMessage; UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true);
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType()); Assert.assertNotNull(userCredentialsMsg);
Assert.assertEquals(savedTenantAdmin.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); Assert.assertEquals(tenantAdminUserId, userCredentialsMsg.getUserId());
Assert.assertEquals(savedTenantAdmin.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB());
testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg);
} }
@Test @Test
public void testCreateUpdateDeleteCustomerUser() throws Exception { public void testSendUserDeleteFromEdgeToCloud() throws Exception {
// create customer // create customer
Customer savedCustomer = createAndAssignCustomerToEdge();
// create user
User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId());
User savedCustomerUser = createAndVerifyUserOnEdge(customerUser);
// simulate user removal event from edge to cloud
UserUpdateMsg.Builder userUpdateMsg = UserUpdateMsg.newBuilder().setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
.setIdMSB(savedCustomerUser.getUuidId().getMostSignificantBits())
.setIdLSB(savedCustomerUser.getUuidId().getLeastSignificantBits());
UplinkMsg uplink = UplinkMsg.newBuilder()
.setUplinkMsgId(EdgeUtils.nextPositiveInt())
.addUserUpdateMsg(userUpdateMsg).build();
testAutoGeneratedCodeByProtobuf(userUpdateMsg);
// expect edge message sent & cloud message response
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplink);
Assert.assertTrue(edgeImitator.waitForResponses());
loginTenantAdmin();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.until(() -> {
try {
doGet("/api/user/" + savedCustomerUser.getId(), User.class, status().isNotFound());
return true;
} catch (Throwable ex) {
return false;
}
});
}
private Customer createAndAssignCustomerToEdge() throws Exception {
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
Customer customer = new Customer(); Customer customer = new Customer();
customer.setTitle("Edge Customer"); customer.setTitle("Edge Customer");
Customer savedCustomer = doPost("/api/customer", customer, Customer.class); Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
Assert.assertFalse(edgeImitator.waitForMessages(5)); Assert.assertFalse(edgeImitator.waitForMessages(5));
// assign edge to customer
edgeImitator.expectMessageAmount(2); edgeImitator.expectMessageAmount(2);
doPost("/api/customer/" + savedCustomer.getUuidId() doPost("/api/customer/" + savedCustomer.getUuidId() + "/edge/" + edge.getUuidId(), Edge.class);
+ "/edge/" + edge.getUuidId(), Edge.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
// create user return savedCustomer;
}
private User buildUser(Authority authority, CustomerId customerId) {
User user = new User();
user.setAuthority(authority);
user.setTenantId(tenantId);
user.setCustomerId(customerId);
user.setEmail(authority == Authority.TENANT_ADMIN ? DEFAULT_TENANT_ADMIN_EMAIL : DEFAULT_CUSTOMER_USER_EMAIL);
user.setFirstName(DEFAULT_FIRST_NAME);
user.setLastName(DEFAULT_LAST_NAME);
return user;
}
private User createAndVerifyUserOnEdge(User user) throws Exception {
String password = user.getAuthority() == Authority.TENANT_ADMIN ? "tenant" : "customer";
// wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user)
edgeImitator.expectMessageAmount(3); edgeImitator.expectMessageAmount(3);
User customerUser = new User(); User savedUser = createUser(user, password);
customerUser.setAuthority(Authority.CUSTOMER_USER); Assert.assertTrue(edgeImitator.waitForMessages());
customerUser.setTenantId(tenantId);
customerUser.setCustomerId(savedCustomer.getId());
customerUser.setEmail("customerUser@thingsboard.org");
customerUser.setFirstName("John");
customerUser.setLastName("Edwards");
User savedCustomerUser = createUser(customerUser, "customer");
Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user)
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size());
Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size());
Optional<UserUpdateMsg> userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(userUpdateMsgOpt.isPresent()); UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg();
UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get();
User userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); User userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true);
Assert.assertNotNull(userMsg); Assert.assertNotNull(userMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType());
Assert.assertEquals(savedCustomerUser.getId(), userMsg.getId()); Assert.assertEquals(savedUser.getId(), userMsg.getId());
Assert.assertEquals(savedCustomerUser.getCustomerId(), userMsg.getCustomerId()); Assert.assertEquals(savedUser.getCustomerId(), userMsg.getCustomerId());
Assert.assertEquals(savedCustomerUser.getAuthority(), userMsg.getAuthority()); Assert.assertEquals(savedUser.getAuthority(), userMsg.getAuthority());
Assert.assertEquals(savedCustomerUser.getEmail(), userMsg.getEmail()); Assert.assertEquals(savedUser.getEmail(), userMsg.getEmail());
Assert.assertEquals(savedCustomerUser.getFirstName(), userMsg.getFirstName()); Assert.assertEquals(savedUser.getFirstName(), userMsg.getFirstName());
Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName()); Assert.assertEquals(savedUser.getLastName(), userMsg.getLastName());
return savedUser;
}
private void updateAndVerifyUserLastName(User user) throws Exception {
user.setLastName(UPDATED_LAST_NAME);
// update user
edgeImitator.expectMessageAmount(2); edgeImitator.expectMessageAmount(2);
savedCustomerUser.setLastName("Addams"); doPost("/api/user", user, User.class);
savedCustomerUser = doPost("/api/user", savedCustomerUser, User.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(userUpdateMsgOpt.isPresent()); UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg();
userUpdateMsg = userUpdateMsgOpt.get(); User userFromMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true);
userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); Assert.assertNotNull(userFromMsg);
Assert.assertNotNull(userMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType());
Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName()); Assert.assertEquals(UPDATED_LAST_NAME, userFromMsg.getLastName());
}
// update user credentials private void updateAndVerifyUserCredentials(User user) throws Exception {
login(savedCustomerUser.getEmail(), "customer"); String password = user.getAuthority() == Authority.TENANT_ADMIN ? "tenant" : "customer";
String newPassword = "new" + password;
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest();
changePasswordRequest.setCurrentPassword("customer"); changePasswordRequest.setCurrentPassword(password);
changePasswordRequest.setNewPassword("newCustomer"); changePasswordRequest.setNewPassword(newPassword);
doPost("/api/auth/changePassword", changePasswordRequest); doPost("/api/auth/changePassword", changePasswordRequest);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg);
UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage;
UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true);
Assert.assertNotNull(userCredentialsMsg);
Assert.assertEquals(savedCustomerUser.getId(), userCredentialsMsg.getUserId());
Assert.assertTrue(passwordEncoder.matches(changePasswordRequest.getNewPassword(), userCredentialsMsg.getPassword()));
loginTenantAdmin(); UserCredentialsUpdateMsg msg = getLatestUserCredentialsUpdateMsg();
UserCredentials creds = JacksonUtil.fromString(msg.getEntity(), UserCredentials.class, true);
Assert.assertNotNull(creds);
Assert.assertEquals(user.getId(), creds.getUserId());
Assert.assertTrue(passwordEncoder.matches(newPassword, creds.getPassword()));
}
// delete user private void deleteAndVerifyUser(User savedTenantAdmin) throws Exception {
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
doDelete("/api/user/" + savedCustomerUser.getUuidId()) doDelete("/api/user/" + savedTenantAdmin.getUuidId())
.andExpect(status().isOk()); .andExpect(status().isOk());
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof UserUpdateMsg); UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg();
userUpdateMsg = (UserUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType());
Assert.assertEquals(savedCustomerUser.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); Assert.assertEquals(savedTenantAdmin.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB());
Assert.assertEquals(savedCustomerUser.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); Assert.assertEquals(savedTenantAdmin.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB());
}
private UplinkMsg buildUserUplinkMsg(UserId userId, CustomerId customerId, UserCredentialsId userCredentialsUuid) {
User customerUser = buildUser(Authority.CUSTOMER_USER, customerId);
customerUser.setId(userId);
UserCredentials userCredentials = buildCredentials(userCredentialsUuid, userId, false);
userCredentials.setActivateToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH));
UserUpdateMsg userUpdateMsg = EdgeMsgConstructorUtils.constructUserUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUser);
UserCredentialsUpdateMsg userCredentialsMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentials);
return UplinkMsg.newBuilder()
.addUserUpdateMsg(userUpdateMsg)
.addUserCredentialsUpdateMsg(userCredentialsMsg)
.build();
} }
@Test private UserCredentials buildCredentials(UserCredentialsId userCredentialsUuid, UserId userId, boolean enabled) {
public void testSendUserCredentialsRequestToCloud() throws Exception { UserCredentials userCredentials = new UserCredentials();
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder();
userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits());
userCredentialsRequestMsgBuilder.setUserIdLSB(tenantAdminUserId.getId().getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(userCredentialsRequestMsgBuilder);
uplinkMsgBuilder.addUserCredentialsRequestMsg(userCredentialsRequestMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); userCredentials.setId(userCredentialsUuid);
userCredentials.setUserId(userId);
userCredentials.setEnabled(enabled);
userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode());
return userCredentials;
}
private User verifyMsgOnCloud(UplinkMsg uplinkMsg, UserId userId, boolean emailExist) throws Exception {
edgeImitator.expectResponsesAmount(1); edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1); edgeImitator.sendUplinkMsg(uplinkMsg);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage(); User userFromCloud = doGet("/api/user/" + userId, User.class);
Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); Assert.assertNotNull(userFromCloud);
UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage;
UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); if (emailExist) {
Assert.assertNotNull(userCredentialsMsg); Assert.assertNotEquals(DEFAULT_CUSTOMER_USER_EMAIL, userFromCloud.getEmail());
Assert.assertEquals(tenantAdminUserId, userCredentialsMsg.getUserId()); } else {
Assert.assertEquals(DEFAULT_CUSTOMER_USER_EMAIL, userFromCloud.getEmail());
}
return userFromCloud;
} }
@Test private UplinkMsg constructUserCredentialsUplinkMsg(UserCredentialsId userCredentialsUuid, UserId userId) {
public void sendUserCredentialsRequest() throws Exception { UserCredentials userCredentials = buildCredentials(userCredentialsUuid, userId, true);
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); userCredentials.setPassword("password");
UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder(); UserCredentialsUpdateMsg credsMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentials);
userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits());
userCredentialsRequestMsgBuilder.setUserIdLSB(tenantAdminUserId.getId().getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(userCredentialsRequestMsgBuilder);
uplinkMsgBuilder.addUserCredentialsRequestMsg(userCredentialsRequestMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); return UplinkMsg.newBuilder()
.addUserCredentialsUpdateMsg(credsMsg)
.build();
}
edgeImitator.expectResponsesAmount(1); private void assertUserCredentialsFlags(User user, boolean enabled, boolean activated) {
edgeImitator.expectMessageAmount(1); JsonNode info = user.getAdditionalInfo();
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); Assert.assertNotNull(info);
Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertEquals(enabled, info.get("userCredentialsEnabled").asBoolean());
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertEquals(activated, info.get("userActivated").asBoolean());
}
AbstractMessage latestMessage = edgeImitator.getLatestMessage(); private UserUpdateMsg getLatestUserUpdateMsg() {
Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); Optional<UserUpdateMsg> opt = edgeImitator.findMessageByType(UserUpdateMsg.class);
UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; Assert.assertTrue(opt.isPresent());
UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); return opt.get();
Assert.assertNotNull(userCredentialsMsg); }
Assert.assertEquals(tenantAdminUserId, userCredentialsMsg.getUserId());
testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); private UserCredentialsUpdateMsg getLatestUserCredentialsUpdateMsg() {
Optional<UserCredentialsUpdateMsg> opt = edgeImitator.findMessageByType(UserCredentialsUpdateMsg.class);
Assert.assertTrue(opt.isPresent());
return opt.get();
} }
} }

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

@ -48,6 +48,8 @@ public interface UserService extends EntityDaoService {
User saveUser(TenantId tenantId, User user); User saveUser(TenantId tenantId, User user);
User saveUser(TenantId tenantId, User user, boolean doValidate);
UserCredentials findUserCredentialsByUserId(TenantId tenantId, UserId userId); UserCredentials findUserCredentialsByUserId(TenantId tenantId, UserId userId);
UserCredentials findUserCredentialsByActivateToken(TenantId tenantId, String activateToken); UserCredentials findUserCredentialsByActivateToken(TenantId tenantId, String activateToken);
@ -56,6 +58,8 @@ public interface UserService extends EntityDaoService {
UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials); UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials);
UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials, boolean doValidate);
UserCredentials activateUserCredentials(TenantId tenantId, String activateToken, String password); UserCredentials activateUserCredentials(TenantId tenantId, String activateToken, String password);
UserCredentials requestPasswordReset(TenantId tenantId, String email); UserCredentials requestPasswordReset(TenantId tenantId, String email);
@ -70,6 +74,8 @@ public interface UserService extends EntityDaoService {
UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials); UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials);
void deleteUserCredentials(TenantId tenantId, UserCredentials userCredentials);
void deleteUser(TenantId tenantId, User user); void deleteUser(TenantId tenantId, User user);
PageData<User> findUsersByTenantId(TenantId tenantId, PageLink pageLink); PageData<User> findUsersByTenantId(TenantId tenantId, PageLink pageLink);

3
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -41,6 +41,7 @@ public class DataConstants {
public static final String EDGE_ID = "edgeId"; public static final String EDGE_ID = "edgeId";
public static final String DEVICE_ID = "deviceId"; public static final String DEVICE_ID = "deviceId";
public static final String GATEWAY_PARAMETER = "gateway"; public static final String GATEWAY_PARAMETER = "gateway";
public static final String CF_NAME_METADATA_KEY = "calculatedFieldName";
public static final String OVERWRITE_ACTIVITY_TIME_PARAMETER = "overwriteActivityTime"; public static final String OVERWRITE_ACTIVITY_TIME_PARAMETER = "overwriteActivityTime";
public static final String COAP_TRANSPORT_NAME = "COAP"; public static final String COAP_TRANSPORT_NAME = "COAP";
@ -105,6 +106,8 @@ public class DataConstants {
public static final String RPC_FAILED = "RPC_FAILED"; public static final String RPC_FAILED = "RPC_FAILED";
public static final String RPC_DELETED = "RPC_DELETED"; public static final String RPC_DELETED = "RPC_DELETED";
public static final String REEVALUATION_MSG = "REEVALUATION_MSG";
public static final String DEFAULT_SECRET_KEY = ""; public static final String DEFAULT_SECRET_KEY = "";
public static final String SECRET_KEY_FIELD_NAME = "secretKey"; public static final String SECRET_KEY_FIELD_NAME = "secretKey";
public static final String DURATION_MS_FIELD_NAME = "durationMs"; public static final String DURATION_MS_FIELD_NAME = "durationMs";

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java

@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoNullChar; import org.thingsboard.server.common.data.validation.NoNullChar;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial; import java.io.Serial;
@ -64,6 +65,7 @@ public final class AiModel extends BaseData<AiModelId> implements HasTenantId, H
@NotBlank @NotBlank
@NoNullChar @NoNullChar
@Length(min = 1, max = 255) @Length(min = 1, max = 255)
@NoXss
@Schema( @Schema(
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE, accessMode = Schema.AccessMode.READ_WRITE,

1
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java

@ -24,6 +24,7 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor @NoArgsConstructor
public class AttributesImmediateOutputStrategy implements AttributesOutputStrategy { public class AttributesImmediateOutputStrategy implements AttributesOutputStrategy {
private boolean sendAttributesUpdatedNotification;
private boolean updateAttributesOnlyOnValueChange; private boolean updateAttributesOnlyOnValueChange;
private boolean saveAttribute; private boolean saveAttribute;

2
common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.util.List; import java.util.List;
@ -42,6 +43,7 @@ public class OAuth2Client extends BaseDataWithAdditionalInfo<OAuth2ClientId> imp
private TenantId tenantId; private TenantId tenantId;
@Schema(description = "Oauth2 client title") @Schema(description = "Oauth2 client title")
@NotBlank @NotBlank
@NoXss
@Length(fieldName = "title", max = 100, message = "cannot be longer than 100 chars") @Length(fieldName = "title", max = 100, message = "cannot be longer than 100 chars")
private String title; private String title;
@Schema(description = "Config for mapping OAuth2 log in response to platform entities", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "Config for mapping OAuth2 log in response to platform entities", requiredMode = Schema.RequiredMode.REQUIRED)

22
common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalTest.java

@ -65,7 +65,7 @@ public class AggIntervalTest {
@ParameterizedTest @ParameterizedTest
@MethodSource("intervals") @MethodSource("intervals")
void testGetStartAndEndWithoutOffset(LongFunction<AggInterval> intervalCreator) { void testGetStartAndEndWithoutOffset(LongFunction<AggInterval> intervalCreator, long expectedDuration) {
AggInterval interval = intervalCreator.apply(0L); AggInterval interval = intervalCreator.apply(0L);
ZonedDateTime dateTime = ZonedDateTime.of( ZonedDateTime dateTime = ZonedDateTime.of(
@ -76,7 +76,7 @@ public class AggIntervalTest {
long endTs = interval.getDateTimeIntervalEndTs(dateTime); long endTs = interval.getDateTimeIntervalEndTs(dateTime);
assertThat(endTs).isGreaterThan(startTs); assertThat(endTs).isGreaterThan(startTs);
assertThat(endTs - startTs).isEqualTo(interval.getCurrentIntervalDurationMillis()); assertThat(endTs - startTs).isEqualTo(expectedDuration);
} }
@ParameterizedTest @ParameterizedTest
@ -88,7 +88,7 @@ public class AggIntervalTest {
ZonedDateTime dateTime = ZonedDateTime.of( ZonedDateTime dateTime = ZonedDateTime.of(
// 2025.11.11 11:20:00 - chosen so 15m offset shifts into a new interval // 2025.11.11 11:20:00 - chosen so 15m offset shifts into a new interval
2025, 11, 11, 11, 20, 0, 0, ZoneId.of(TZ) 2025, 6, 6, 6, 20, 0, 0, ZoneId.of(TZ)
); );
long startWithOffsetTs = intervalWithOffset.getDateTimeIntervalStartTs(dateTime); long startWithOffsetTs = intervalWithOffset.getDateTimeIntervalStartTs(dateTime);
@ -103,14 +103,14 @@ public class AggIntervalTest {
private static Stream<Arguments> intervals() { private static Stream<Arguments> intervals() {
return Stream.of( return Stream.of(
Arguments.of((LongFunction<AggInterval>) offset -> new HourInterval(TZ, offset)), Arguments.of((LongFunction<AggInterval>) offset -> new HourInterval(TZ, offset), TimeUnit.HOURS.toMillis(1)),
Arguments.of((LongFunction<AggInterval>) offset -> new DayInterval(TZ, offset)), Arguments.of((LongFunction<AggInterval>) offset -> new DayInterval(TZ, offset), TimeUnit.DAYS.toMillis(1)),
Arguments.of((LongFunction<AggInterval>) offset -> new WeekInterval(TZ, offset)), Arguments.of((LongFunction<AggInterval>) offset -> new WeekInterval(TZ, offset), TimeUnit.DAYS.toMillis(7)),
Arguments.of((LongFunction<AggInterval>) offset -> new WeekSunSatInterval(TZ, offset)), Arguments.of((LongFunction<AggInterval>) offset -> new WeekSunSatInterval(TZ, offset), TimeUnit.DAYS.toMillis(7)),
Arguments.of((LongFunction<AggInterval>) offset -> new MonthInterval(TZ, offset)), Arguments.of((LongFunction<AggInterval>) offset -> new MonthInterval(TZ, offset), TimeUnit.DAYS.toMillis(30)),
Arguments.of((LongFunction<AggInterval>) offset -> new QuarterInterval(TZ, offset)), Arguments.of((LongFunction<AggInterval>) offset -> new QuarterInterval(TZ, offset), TimeUnit.DAYS.toMillis(92) + TimeUnit.HOURS.toMillis(1)),// Includes DST fallback (2025-10-26), so duration = 92 days + 1 hour(expected for Europe/Kyiv timezone).
Arguments.of((LongFunction<AggInterval>) offset -> new YearInterval(TZ, offset)), Arguments.of((LongFunction<AggInterval>) offset -> new YearInterval(TZ, offset), TimeUnit.DAYS.toMillis(365)),
Arguments.of((LongFunction<AggInterval>) offset -> new CustomInterval(TZ, offset, TimeUnit.HOURS.toSeconds(4))) Arguments.of((LongFunction<AggInterval>) offset -> new CustomInterval(TZ, offset, TimeUnit.HOURS.toSeconds(4)), TimeUnit.HOURS.toMillis(4))
); );
} }

2
common/edge-api/src/main/proto/edge.proto

@ -449,6 +449,8 @@ message UplinkMsg {
repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25; repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25;
repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26; repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26;
repeated AiModelUpdateMsg aiModelUpdateMsg = 27; repeated AiModelUpdateMsg aiModelUpdateMsg = 27;
repeated UserUpdateMsg userUpdateMsg = 28;
repeated UserCredentialsUpdateMsg userCredentialsUpdateMsg = 29;
} }
message UplinkResponseMsg { message UplinkResponseMsg {

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

@ -96,8 +96,9 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
public static final String USER_PASSWORD_HISTORY = "userPasswordHistory"; public static final String USER_PASSWORD_HISTORY = "userPasswordHistory";
private static final int DEFAULT_TOKEN_LENGTH = 30; public static final int DEFAULT_TOKEN_LENGTH = 30;
public static final String INCORRECT_USER_ID = "Incorrect userId "; public static final String INCORRECT_USER_ID = "Incorrect userId ";
public static final String INCORRECT_USER_CREDENTIALS_ID = "Incorrect userCredentialsId ";
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
@Value("${security.user_login_case_sensitive:true}") @Value("${security.user_login_case_sensitive:true}")
@ -173,12 +174,23 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
@Override @Override
@Transactional @Transactional
public User saveUser(TenantId tenantId, User user) { public User saveUser(TenantId tenantId, User user) {
return saveEntity(user, () -> doSaveUser(tenantId, user)); return saveUser(tenantId, user, true);
} }
private User doSaveUser(TenantId tenantId, User user) { @Override
@Transactional
public User saveUser(TenantId tenantId, User user, boolean doValidate) {
return saveEntity(user, () -> doSaveUser(tenantId, user, doValidate));
}
private User doSaveUser(TenantId tenantId, User user, boolean doValidate) {
log.trace("Executing saveUser [{}]", user); log.trace("Executing saveUser [{}]", user);
User oldUser = userValidator.validate(user, User::getTenantId); User oldUser = null;
if (doValidate) {
oldUser = userValidator.validate(user, User::getTenantId);
} else if (user.getId() != null) {
oldUser = findUserById(user.getTenantId(), user.getId());
}
if (!userLoginCaseSensitive) { if (!userLoginCaseSensitive) {
user.setEmail(user.getEmail().toLowerCase()); user.setEmail(user.getEmail().toLowerCase());
} }
@ -233,8 +245,15 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
@Override @Override
public UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials) { public UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials) {
return saveUserCredentials(tenantId, userCredentials, true);
}
@Override
public UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials, boolean doValidate) {
log.trace("Executing saveUserCredentials [{}]", userCredentials); log.trace("Executing saveUserCredentials [{}]", userCredentials);
userCredentialsValidator.validate(userCredentials, data -> tenantId); if (doValidate) {
userCredentialsValidator.validate(userCredentials, data -> tenantId);
}
UserCredentials result = userCredentialsDao.save(tenantId, userCredentials); UserCredentials result = userCredentialsDao.save(tenantId, userCredentials);
eventPublisher.publishEvent(ActionEntityEvent.builder() eventPublisher.publishEvent(ActionEntityEvent.builder()
.tenantId(tenantId) .tenantId(tenantId)
@ -337,6 +356,15 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
return result; return result;
} }
@Override
public void deleteUserCredentials(TenantId tenantId, UserCredentials userCredentials) {
Objects.requireNonNull(userCredentials, "UserCredentials is null");
UserCredentialsId userCredentialsId = userCredentials.getId();
log.trace("[{}] Executing deleteUserCredentials [{}]", tenantId, userCredentialsId);
validateId(userCredentialsId, id -> INCORRECT_USER_CREDENTIALS_ID + id);
userCredentialsDao.removeById(tenantId, userCredentialsId.getId());
}
@Override @Override
@Transactional @Transactional
public void deleteUser(TenantId tenantId, User user) { public void deleteUser(TenantId tenantId, User user) {

4
ui-ngx/src/app/core/auth/auth.service.ts

@ -168,8 +168,8 @@ export class AuthService {
)); ));
} }
public getUserPasswordPolicy() { public getUserPasswordPolicy(config?: RequestConfig) {
return this.http.get<UserPasswordPolicy>(`/api/noauth/userPasswordPolicy`, defaultHttpOptions()); return this.http.get<UserPasswordPolicy>(`/api/noauth/userPasswordPolicy`, defaultHttpOptionsFromConfig(config));
} }
public activateByEmailCode(emailCode: string): Observable<LoginResponse> { public activateByEmailCode(emailCode: string): Observable<LoginResponse> {

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

@ -168,9 +168,8 @@ export class DashboardUtilsService {
dashboard.configuration.filters = {}; dashboard.configuration.filters = {};
} }
if (isUndefined(dashboard.configuration.timewindow)) { dashboard.configuration.timewindow = initModelFromDefaultTimewindow(dashboard.configuration.timewindow,
dashboard.configuration.timewindow = this.timeService.defaultTimewindow(true); false, false, this.timeService, true, true);
}
if (isUndefined(dashboard.configuration.settings)) { if (isUndefined(dashboard.configuration.settings)) {
dashboard.configuration.settings = {}; dashboard.configuration.settings = {};
dashboard.configuration.settings.stateControllerId = 'entity'; dashboard.configuration.settings.stateControllerId = 'entity';

16
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts

@ -67,7 +67,7 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
debugSettings: [], debugSettings: [],
entityId: this.fb.group({ entityId: this.fb.group({
entityType: this.fb.control<EntityType | AliasEntityType | null>(null, Validators.required), entityType: this.fb.control<EntityType | AliasEntityType | null>(null, Validators.required),
id: ['', Validators.required], id: [null as null | string, Validators.required],
}), }),
configuration: this.fb.group({ configuration: this.fb.group({
arguments: this.fb.control({}), arguments: this.fb.control({}),
@ -101,7 +101,6 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private fb: FormBuilder) { private fb: FormBuilder) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.observeIsLoading();
this.applyDialogData(); this.applyDialogData();
} }
@ -178,19 +177,6 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, entityId, ...value }, {emitEvent: false}); this.fieldFormGroup.patchValue({ configuration, type, debugSettings, entityId, ...value }, {emitEvent: false});
} }
private observeIsLoading(): void {
this.isLoading$.pipe(takeUntilDestroyed()).subscribe(loading => {
if (loading) {
this.fieldFormGroup.disable({emitEvent: false});
} else {
this.fieldFormGroup.enable({emitEvent: false});
if (this.data.isDirty) {
this.fieldFormGroup.markAsDirty();
}
}
});
}
onTestScript(expression: string): Observable<string> { onTestScript(expression: string): Observable<string> {
const calculatedFieldId = this.data.value?.id?.id; const calculatedFieldId = this.data.value?.id?.id;
if (calculatedFieldId) { if (calculatedFieldId) {

2
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts

@ -152,7 +152,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
this.cellActionDescriptors.push( this.cellActionDescriptors.push(
{ {
name: this.translate.instant('notification.copy-template'), name: this.translate.instant('alarm-rule.copy'),
icon: 'content_copy', icon: 'content_copy',
isEnabled: () => true, isEnabled: () => true,
onAction: ($event, entity) => this.copyCalculatedField(entity) onAction: ($event, entity) => this.copyCalculatedField(entity)

10
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts

@ -20,7 +20,7 @@ import {
FormBuilder, FormBuilder,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormControl, ValidationErrors,
Validator Validator
} from '@angular/forms'; } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@ -130,10 +130,10 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
} }
public conditionSet() { public conditionSet() {
return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters) || !this.required; return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters);
} }
public validate(c: UntypedFormControl) { public validate(): ValidationErrors | null {
return this.conditionSet() ? null : { return this.conditionSet() ? null : {
alarmRuleCondition: { alarmRuleCondition: {
valid: false, valid: false,
@ -166,11 +166,11 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
private updateConditionInfo() { private updateConditionInfo() {
this.alarmRuleConditionFormGroup.patchValue( this.alarmRuleConditionFormGroup.patchValue(
{ this.modelValue ? {
type: this.modelValue?.type, type: this.modelValue?.type,
expression: this.modelValue?.expression, expression: this.modelValue?.expression,
schedule: this.modelValue?.schedule, schedule: this.modelValue?.schedule,
}, {emitEvent: false} } : null, {emitEvent: false}
); );
this.updateScheduleText(); this.updateScheduleText();
this.updateSpecText(); this.updateSpecText();

23
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts

@ -20,8 +20,9 @@ import {
FormBuilder, FormBuilder,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormControl, ValidationErrors,
Validator Validator,
Validators
} from '@angular/forms'; } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
@ -76,7 +77,7 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid
private modelValue: AlarmRule; private modelValue: AlarmRule;
alarmRuleFormGroup = this.fb.group({ alarmRuleFormGroup = this.fb.group({
condition: this.fb.control<AlarmRuleCondition | null>(null), condition: this.fb.control<AlarmRuleCondition | null>(null, Validators.required),
alarmDetails: [null], alarmDetails: [null],
dashboardId: [null] dashboardId: [null]
}); });
@ -113,14 +114,12 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid
} }
writeValue(value: AlarmRule): void { writeValue(value: AlarmRule): void {
if (value) { this.modelValue = value;
this.modelValue = value; const model = this.modelValue ? {
const model = this.modelValue ? { ...this.modelValue,
...this.modelValue, dashboardId: this.modelValue.dashboardId?.id
dashboardId: this.modelValue.dashboardId?.id } : null;
} : null; this.alarmRuleFormGroup.patchValue(model, {emitEvent: false});
this.alarmRuleFormGroup.patchValue(model, {emitEvent: false});
}
} }
public openEditDetailsDialog($event: Event) { public openEditDetailsDialog($event: Event) {
@ -142,7 +141,7 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid
}); });
} }
public validate(c: UntypedFormControl) { public validate(): ValidationErrors | null {
return (!this.required && !this.modelValue || this.alarmRuleFormGroup.valid) ? null : { return (!this.required && !this.modelValue || this.alarmRuleFormGroup.valid) ? null : {
alarmRule: { alarmRule: {
valid: false, valid: false,

6
ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts

@ -23,7 +23,7 @@ import {
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormArray, UntypedFormArray,
UntypedFormControl, ValidationErrors,
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
@ -159,8 +159,8 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida
return null; return null;
} }
public validate(c: UntypedFormControl) { public validate(): ValidationErrors | null {
return this.createAlarmRulesFormArray().length ? null : { return this.createAlarmRulesFormGroup.valid && this.createAlarmRulesFormArray().length > 0 ? null : {
createAlarmRules: { createAlarmRules: {
valid: false, valid: false,
}, },

5
ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html

@ -35,7 +35,10 @@
</div> </div>
<mat-form-field class="mat-block" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="mat-block" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>api-key.description</mat-label> <mat-label translate>api-key.description</mat-label>
<textarea #input cdkTextareaAutosize matInput formControlName="description" rows="2" maxLength="255"></textarea> <textarea cdkTextareaAutosize matInput formControlName="description" rows="2" maxLength="255"></textarea>
<mat-error *ngIf="apiKeyForm.get('description').hasError('required')">
{{ 'asset.description-required' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<mat-slide-toggle class="mat-slide" formControlName="enabled"> <mat-slide-toggle class="mat-slide" formControlName="enabled">
{{ 'api-key.enable' | translate }} {{ 'api-key.enable' | translate }}

4
ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.html

@ -96,6 +96,10 @@
<ng-template #executeCommand> <ng-template #executeCommand>
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>device.connectivity.execute-following-command</div> <div class="tb-form-panel-title" translate>device.connectivity.execute-following-command</div>
<div class="tb-form-hint tb-primary-fill insecure-url-warning flex gap-2" [class.hidden]="secureUrl">
<mat-icon class="tb-mat-24">warning</mat-icon>
<span>{{ 'api-key.generated-api-key-insecure-url' | translate }}</span>
</div>
<tb-markdown usePlainMarkdown containerClass="tb-command-code" [data]='createMarkDownCommand(apiKeyCommand)'></tb-markdown> <tb-markdown usePlainMarkdown containerClass="tb-command-code" [data]='createMarkDownCommand(apiKeyCommand)'></tb-markdown>
</div> </div>
</ng-template> </ng-template>

17
ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.scss

@ -15,9 +15,8 @@
*/ */
@import '../../src/scss/constants'; @import '../../src/scss/constants';
:host{ :host {
display: grid; display: grid;
width: 500px;
height: 100%; height: 100%;
max-width: 100%; max-width: 100%;
max-height: 100vh; max-height: 100vh;
@ -26,6 +25,20 @@
.tb-install-instruction-text { .tb-install-instruction-text {
min-height: 42px; min-height: 42px;
} }
.insecure-url-warning {
.mat-icon {
color: #FAA405;
}
}
@media #{$mat-sm} {
width: 470px;
}
@media #{$mat-gt-sm} {
width: 720px;
}
} }
:host ::ng-deep { :host ::ng-deep {

5
ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.ts

@ -34,7 +34,10 @@ export interface ApiKeyGeneratedDialogData {
}) })
export class ApiKeyGeneratedDialogComponent extends DialogComponent<ApiKeyGeneratedDialogComponent, void> { export class ApiKeyGeneratedDialogComponent extends DialogComponent<ApiKeyGeneratedDialogComponent, void> {
apiKeyCommand = userInfoCommand(this.data.apiKey.value); private baseUrl = window.location.origin;
apiKeyCommand = userInfoCommand(this.baseUrl, this.data.apiKey.value);
secureUrl = this.baseUrl.startsWith('https');
selectedTab: number; selectedTab: number;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,

5
ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html

@ -21,7 +21,10 @@
<mat-form-field class="mat-block" appearance="outline"> <mat-form-field class="mat-block" appearance="outline">
<mat-label translate>api-key.description</mat-label> <mat-label translate>api-key.description</mat-label>
<textarea #input cdkTextareaAutosize matInput [formControl]="descriptionFormControl" rows="2" maxLength="255"></textarea> <textarea #input cdkTextareaAutosize matInput [formControl]="descriptionFormControl" rows="2" maxLength="255"></textarea>
<mat-hint align="end">{{input.value?.length || 0}}/255</mat-hint> <mat-error *ngIf="descriptionFormControl.hasError('required')">
{{ 'asset.description-required' | translate }}
</mat-error>
<mat-hint align="end">{{ input.value?.length || 0 }}/255</mat-hint>
</mat-form-field> </mat-form-field>
<div class="tb-edit-api-key-description-panel-buttons"> <div class="tb-edit-api-key-description-panel-buttons">
<button mat-button <button mat-button

4
ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.ts

@ -16,7 +16,7 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder, Validators } from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { ApiKeyService } from '@core/http/api-key.service'; import { ApiKeyService } from '@core/http/api-key.service';
@ -37,7 +37,7 @@ export class EditApiKeyDescriptionPanelComponent implements OnInit {
@Output() @Output()
descriptionApplied = new EventEmitter<string>(); descriptionApplied = new EventEmitter<string>();
descriptionFormControl = this.fb.control<string>(null); descriptionFormControl = this.fb.control<string>(null, Validators.required);
constructor(private fb: FormBuilder, constructor(private fb: FormBuilder,
private popover: TbPopoverComponent<EditApiKeyDescriptionPanelComponent>, private popover: TbPopoverComponent<EditApiKeyDescriptionPanelComponent>,

2
ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts

@ -162,7 +162,7 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor {
reset() { reset() {
if (this.initialAuditLogFilter) { if (this.initialAuditLogFilter) {
if (!auditLogFilterEquals(this.auditLogFilter, this.initialAuditLogFilter)) { if (!auditLogFilterEquals(this.auditLogFilterForm.value, this.initialAuditLogFilter)) {
this.updateAuditLogFilterForm(this.initialAuditLogFilter); this.updateAuditLogFilterForm(this.initialAuditLogFilter);
this.auditLogFilterForm.markAsDirty(); this.auditLogFilterForm.markAsDirty();
} }

38
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html

@ -95,40 +95,37 @@
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
@if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) { @if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) {
@if (outputForm.get('type').value === OutputType.Timeseries) { <div class="tb-form-panel stroked">
<div class="tb-form-row flex-1"> <div class="tb-form-panel-title" translate>calculated-fields.output-strategy.processing-parameters</div>
@if (outputForm.get('type').value === OutputType.Timeseries) {
<mat-slide-toggle class="mat-slide" formControlName="saveTimeSeries"> <mat-slide-toggle class="mat-slide" formControlName="saveTimeSeries">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-time-series' | translate }}"> <div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-time-series' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-time-series</div> <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-time-series</div>
</div> </div>
</mat-slide-toggle> </mat-slide-toggle>
</div>
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="saveLatest"> <mat-slide-toggle class="mat-slide" formControlName="saveLatest">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-latest-values' | translate }}"> <div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-latest-values' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-latest-values</div> <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-latest-values</div>
</div> </div>
</mat-slide-toggle> </mat-slide-toggle>
</div> } @else {
} @else {
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="saveAttribute"> <mat-slide-toggle class="mat-slide" formControlName="saveAttribute">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-database' | translate }}"> <div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-database' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-database</div> <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-database</div>
</div> </div>
</mat-slide-toggle> </mat-slide-toggle>
</div> }
}
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="sendWsUpdate"> <mat-slide-toggle class="mat-slide" formControlName="sendWsUpdate">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.send-web-sockets' | translate }}"> <div tb-hint-tooltip-icon="{{ (outputForm.get('type').value === OutputType.Attribute
? 'calculated-fields.output-strategy.hint.send-web-sockets-attribute'
: 'calculated-fields.output-strategy.hint.send-web-sockets-time-series') | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.send-web-sockets</div> <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.send-web-sockets</div>
</div> </div>
</mat-slide-toggle> </mat-slide-toggle>
</div>
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="processCfs"> <mat-slide-toggle class="mat-slide" formControlName="processCfs">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-calculated-fields' | translate }}"> <div tb-hint-tooltip-icon="{{ (outputForm.get('type').value === OutputType.Attribute
? 'calculated-fields.output-strategy.hint.save-calculated-fields-attribute'
: 'calculated-fields.output-strategy.hint.save-calculated-fields-time-series') | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-calculated-fields</div> <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-calculated-fields</div>
</div> </div>
</mat-slide-toggle> </mat-slide-toggle>
@ -137,9 +134,16 @@
<div class="tb-form-row flex-1"> <div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="updateAttributesOnlyOnValueChange"> <mat-slide-toggle class="mat-slide" formControlName="updateAttributesOnlyOnValueChange">
<div tb-hint-tooltip-icon="{{ (outputForm.get('strategy.updateAttributesOnlyOnValueChange').value <div tb-hint-tooltip-icon="{{ (outputForm.get('strategy.updateAttributesOnlyOnValueChange').value
? 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change-enabled' ? 'calculated-fields.output-strategy.hint.update-attribute-only-on-value-change-enabled'
: 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change') | translate }}"> : 'calculated-fields.output-strategy.hint.update-attribute-only-on-value-change') | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.update-attributes-only-on-value-change</div> <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.update-attribute-only-on-value-change</div>
</div>
</mat-slide-toggle>
</div>
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="sendAttributesUpdatedNotification">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.send-attributes-updated-notification' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.send-attributes-updated-notification</div>
</div> </div>
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts

@ -104,6 +104,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
sendWsUpdate: [true], sendWsUpdate: [true],
processCfs: [true], processCfs: [true],
updateAttributesOnlyOnValueChange: [true], updateAttributesOnlyOnValueChange: [true],
sendAttributesUpdatedNotification: [false],
useCustomTtl: [false], useCustomTtl: [false],
ttl: [0] ttl: [0]
}) })
@ -230,6 +231,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
if (outputType === OutputType.Attribute) { if (outputType === OutputType.Attribute) {
this.outputForm.get('strategy.saveAttribute').enable({emitEvent: false}); this.outputForm.get('strategy.saveAttribute').enable({emitEvent: false});
this.outputForm.get('strategy.updateAttributesOnlyOnValueChange').enable({emitEvent: false}); this.outputForm.get('strategy.updateAttributesOnlyOnValueChange').enable({emitEvent: false});
this.outputForm.get('strategy.sendAttributesUpdatedNotification').enable({emitEvent: false});
} else { } else {
this.outputForm.get('strategy.saveTimeSeries').enable({emitEvent: false}); this.outputForm.get('strategy.saveTimeSeries').enable({emitEvent: false});
this.outputForm.get('strategy.saveLatest').enable({emitEvent: false}); this.outputForm.get('strategy.saveLatest').enable({emitEvent: false});

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

@ -34,7 +34,7 @@ import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { AuthUser } from '@shared/models/user.model'; import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models'; import { initModelFromDefaultTimewindow, Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models';
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
import { GridsterComponent, GridsterConfig, GridType } from 'angular-gridster2'; import { GridsterComponent, GridsterConfig, GridType } from 'angular-gridster2';
import { import {
@ -223,9 +223,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
ngOnInit(): void { ngOnInit(): void {
this.dashboardWidgets.parentDashboard = this.parentDashboard; this.dashboardWidgets.parentDashboard = this.parentDashboard;
this.dashboardWidgets.popoverComponent = this.popoverComponent; this.dashboardWidgets.popoverComponent = this.popoverComponent;
if (!this.dashboardTimewindow) { this.dashboardTimewindow = initModelFromDefaultTimewindow(this.dashboardTimewindow,
this.dashboardTimewindow = this.timeService.defaultTimewindow(true); false, false, this.timeService, true, true);
}
this.gridsterOpts = { this.gridsterOpts = {
gridType: this.gridType || GridType.ScrollVertical, gridType: this.gridType || GridType.ScrollVertical,
keepFixedHeightInMobile: true, keepFixedHeightInMobile: true,

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

@ -943,15 +943,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
entityLabelColumnTitle entityLabelColumnTitle
} = this.modelValue.config.settings; } = this.modelValue.config.settings;
const displayEntitiesArray = []; const displayEntitiesArray = [];
if (isDefined(displayEntityName)) { if (displayEntityName) {
const displayName = entityNameColumnTitle ? entityNameColumnTitle : 'entityName'; const displayName = entityNameColumnTitle ? entityNameColumnTitle : 'entityName';
displayEntitiesArray.push({name: displayName, label: displayName}); displayEntitiesArray.push({name: displayName, label: displayName});
} }
if (isDefined(displayEntityLabel)) { if (displayEntityLabel) {
const displayLabel = entityLabelColumnTitle ? entityLabelColumnTitle : 'entityLabel'; const displayLabel = entityLabelColumnTitle ? entityLabelColumnTitle : 'entityLabel';
displayEntitiesArray.push({name: displayLabel, label: displayLabel}); displayEntitiesArray.push({name: displayLabel, label: displayLabel});
} }
if (isDefined(displayEntityType)) { if (displayEntityType) {
displayEntitiesArray.push({name: 'entityType', label: 'entityType'}); displayEntitiesArray.push({name: 'entityType', label: 'entityType'});
} }
configuredColumns.push(...displayEntitiesArray, ...this.keysToCellClickColumns(this.modelValue.config.datasources[0].dataKeys)); configuredColumns.push(...displayEntitiesArray, ...this.keysToCellClickColumns(this.modelValue.config.datasources[0].dataKeys));

14
ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html

@ -136,7 +136,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row tb-standard-fields no-border no-padding column-xs" *ngIf="isCustomProvider()"> <div class="grid gt-xs:grid-cols-2 gt-xs:gap-4" *ngIf="isCustomProvider()">
<mat-form-field class="flex" floatLabel="always"> <mat-form-field class="flex" floatLabel="always">
<mat-label translate>admin.oauth2.login-button-label</mat-label> <mat-label translate>admin.oauth2.login-button-label</mat-label>
<input matInput formControlName="loginButtonLabel" <input matInput formControlName="loginButtonLabel"
@ -145,10 +145,14 @@
{{ 'admin.oauth2.login-button-label-required' | translate }} {{ 'admin.oauth2.login-button-label-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="flex"> <div class="tb-form-row justify-between" style="height: auto; margin-bottom: 22px;">
<mat-label translate>admin.oauth2.login-button-icon</mat-label> <div translate>admin.oauth2.login-button-icon</div>
<input matInput formControlName="loginButtonIcon"> <tb-material-icon-select asBoxInput
</mat-form-field> iconClearButton
allowedCustomIcon
formControlName="loginButtonIcon">
</tb-material-icon-select>
</div>
</div> </div>
<section formGroupName="mapperConfig" style="margin-bottom: 22px"> <section formGroupName="mapperConfig" style="margin-bottom: 22px">
<div class="tb-form-row no-border no-padding"> <div class="tb-form-row no-border no-padding">

4
ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts

@ -85,8 +85,8 @@ export class DomainTableConfigResolver {
this.config.loadEntity = id => this.domainService.getDomainInfoById(id.id); this.config.loadEntity = id => this.domainService.getDomainInfoById(id.id);
this.config.saveEntity = (domain, originalDomain) => { this.config.saveEntity = (domain, originalDomain) => {
const clientsIds = domain.oauth2ClientInfos as Array<string> || []; const clientsIds = domain.oauth2ClientInfos as Array<string> || [];
const shouldUpdateClients = domain.id && !isEqual(domain.oauth2ClientInfos?.sort(), const shouldUpdateClients = domain.id && !isEqual(domain.oauth2ClientInfos,
originalDomain.oauth2ClientInfos?.map(info => info.id ? info.id.id : info).sort()); originalDomain.oauth2ClientInfos?.map(info => info.id ? info.id.id : info));
delete domain.oauth2ClientInfos; delete domain.oauth2ClientInfos;
return this.domainService.saveDomain(domain, domain.id ? null : clientsIds).pipe( return this.domainService.saveDomain(domain, domain.id ? null : clientsIds).pipe(

10
ui-ngx/src/app/modules/home/pages/security/security.component.html

@ -54,7 +54,7 @@
<mat-label translate>profile.current-password</mat-label> <mat-label translate>profile.current-password</mat-label>
<input matInput type="password" name="current-password" formControlName="currentPassword" autocomplete="current-password"/> <input matInput type="password" name="current-password" formControlName="currentPassword" autocomplete="current-password"/>
<tb-toggle-password [class.!hidden]="!changePassword.get('currentPassword').dirty && !changePassword.get('currentPassword').touched" matSuffix></tb-toggle-password> <tb-toggle-password [class.!hidden]="!changePassword.get('currentPassword').dirty && !changePassword.get('currentPassword').touched" matSuffix></tb-toggle-password>
<mat-error *ngIf="changePassword.get('currentPassword').hasError('differencePassword')"> <mat-error *ngIf="changePassword.get('currentPassword').hasError('passwordsNotMatch')">
{{ 'security.password-requirement.incorrect-password-try-again' | translate }} {{ 'security.password-requirement.incorrect-password-try-again' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -66,13 +66,13 @@
<mat-error *ngIf="changePassword.get('newPassword').errors <mat-error *ngIf="changePassword.get('newPassword').errors
&& !changePassword.get('newPassword').hasError('alreadyUsed') && !changePassword.get('newPassword').hasError('alreadyUsed')
&& !changePassword.get('newPassword').hasError('hasWhitespaces') && !changePassword.get('newPassword').hasError('hasWhitespaces')
&& !changePassword.get('newPassword').hasError('samePassword')"> && !changePassword.get('newPassword').hasError('passwordSameAsOld')">
{{ 'security.password-requirement.password-not-meet-requirements' | translate }} {{ 'security.password-requirement.password-not-meet-requirements' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="changePassword.get('newPassword').hasError('alreadyUsed')"> <mat-error *ngIf="changePassword.get('newPassword').hasError('alreadyUsed')">
{{ changePassword.get('newPassword').getError('alreadyUsed') }} {{ changePassword.get('newPassword').getError('alreadyUsed') }}
</mat-error> </mat-error>
<mat-error *ngIf="changePassword.get('newPassword').hasError('samePassword')"> <mat-error *ngIf="changePassword.get('newPassword').hasError('passwordSameAsOld')">
{{ 'security.password-requirement.password-should-difference' | translate }} {{ 'security.password-requirement.password-should-difference' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="changePassword.get('newPassword').hasError('hasWhitespaces')"> <mat-error *ngIf="changePassword.get('newPassword').hasError('hasWhitespaces')">
@ -86,7 +86,7 @@
<mat-label translate>login.new-password-again</mat-label> <mat-label translate>login.new-password-again</mat-label>
<input matInput type="password" name="new-password" formControlName="newPassword2" autocomplete="new-password" required/> <input matInput type="password" name="new-password" formControlName="newPassword2" autocomplete="new-password" required/>
<tb-toggle-password [class.!hidden]="!changePassword.get('newPassword2').dirty && !changePassword.get('newPassword2').touched" matSuffix></tb-toggle-password> <tb-toggle-password [class.!hidden]="!changePassword.get('newPassword2').dirty && !changePassword.get('newPassword2').touched" matSuffix></tb-toggle-password>
<mat-error *ngIf="changePassword.get('newPassword2').hasError('differencePassword')"> <mat-error *ngIf="changePassword.get('newPassword2').hasError('passwordsNotMatch')">
{{ 'security.password-requirement.new-passwords-not-match' | translate }} {{ 'security.password-requirement.new-passwords-not-match' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -148,7 +148,7 @@
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async)"> [disabled]="(isLoading$ | async) || (!changePassword.valid && changePassword.touched)">
{{ 'profile.change-password' | translate }} {{ 'profile.change-password' | translate }}
</button> </button>
</div> </div>

82
ui-ngx/src/app/modules/home/pages/security/security.component.ts

@ -21,9 +21,9 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { import {
AbstractControl, AbstractControl,
FormGroupDirective,
UntypedFormBuilder, UntypedFormBuilder,
UntypedFormGroup, FormGroupDirective, UntypedFormGroup,
NgForm,
ValidationErrors, ValidationErrors,
ValidatorFn, ValidatorFn,
Validators Validators
@ -48,7 +48,7 @@ import {
import { authenticationDialogMap } from '@home/pages/security/authentication-dialog/authentication-dialog.map'; import { authenticationDialogMap } from '@home/pages/security/authentication-dialog/authentication-dialog.map';
import { takeUntil, tap } from 'rxjs/operators'; import { takeUntil, tap } from 'rxjs/operators';
import { Observable, of, Subject } from 'rxjs'; import { Observable, of, Subject } from 'rxjs';
import { isDefinedAndNotNull, isEqual } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { AuthService } from '@core/auth/auth.service'; import { AuthService } from '@core/auth/auth.service';
import { UserPasswordPolicy } from '@shared/models/settings.models'; import { UserPasswordPolicy } from '@shared/models/settings.models';
import { MatCheckboxChange } from '@angular/material/checkbox'; import { MatCheckboxChange } from '@angular/material/checkbox';
@ -56,6 +56,7 @@ import {
ApiKeysTableDialogComponent, ApiKeysTableDialogComponent,
ApiKeysTableDialogData ApiKeysTableDialogData
} from '@home/components/api-key/api-keys-table-dialog.component'; } from '@home/components/api-key/api-keys-table-dialog.component';
import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/models/password.models';
@Component({ @Component({
selector: 'tb-security', selector: 'tb-security',
@ -168,7 +169,12 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
this.changePassword = this.fb.group({ this.changePassword = this.fb.group({
currentPassword: [''], currentPassword: [''],
newPassword: ['', Validators.required], newPassword: ['', Validators.required],
newPassword2: ['', this.samePasswordValidation(false, 'newPassword')] newPassword2: ['']
}, {
validators: [
this.passwordNotSameAsOld(),
passwordsMatchValidator('newPassword', 'newPassword2'),
]
}); });
} }
@ -176,64 +182,36 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
this.authService.getUserPasswordPolicy().subscribe(policy => { this.authService.getUserPasswordPolicy().subscribe(policy => {
this.passwordPolicy = policy; this.passwordPolicy = policy;
this.changePassword.get('newPassword').setValidators([ this.changePassword.get('newPassword').setValidators([
this.passwordStrengthValidator(), passwordStrengthValidator(this.passwordPolicy),
this.samePasswordValidation(true, 'currentPassword'),
Validators.required Validators.required
]); ]);
this.changePassword.get('newPassword').updateValueAndValidity({emitEvent: false}); this.changePassword.get('newPassword').updateValueAndValidity({emitEvent: false});
}); });
} }
private passwordStrengthValidator(): ValidatorFn { passwordNotSameAsOld(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => { return (group: AbstractControl): ValidationErrors | null => {
const value: string = control.value; const currentPassControl = group.get('currentPassword');
const errors: any = {}; const newPassControl = group.get('newPassword');
if (this.passwordPolicy.minimumUppercaseLetters > 0 &&
!new RegExp(`(?:.*?[A-Z]){${this.passwordPolicy.minimumUppercaseLetters}}`).test(value)) {
errors.notUpperCase = true;
}
if (this.passwordPolicy.minimumLowercaseLetters > 0 &&
!new RegExp(`(?:.*?[a-z]){${this.passwordPolicy.minimumLowercaseLetters}}`).test(value)) {
errors.notLowerCase = true;
}
if (this.passwordPolicy.minimumDigits > 0
&& !new RegExp(`(?:.*?\\d){${this.passwordPolicy.minimumDigits}}`).test(value)) {
errors.notNumeric = true;
}
if (this.passwordPolicy.minimumSpecialCharacters > 0 && const current = currentPassControl?.value ?? '';
!new RegExp(`(?:.*?[\\W_]){${this.passwordPolicy.minimumSpecialCharacters}}`).test(value)) { const newPass = newPassControl?.value ?? '';
errors.notSpecial = true;
}
if (!this.passwordPolicy.allowWhitespaces && /\s/.test(value)) {
errors.hasWhitespaces = true;
}
if (this.passwordPolicy.minimumLength > 0 && value.length < this.passwordPolicy.minimumLength) {
errors.minLength = true;
}
if (!value.length || this.passwordPolicy.maximumLength > 0 && value.length > this.passwordPolicy.maximumLength) { if (current && newPass && current === newPass) {
errors.maxLength = true; newPassControl?.setErrors({
} ...newPassControl.errors,
passwordSameAsOld: true
return isEqual(errors, {}) ? null : errors; });
}; return { passwordSameAsOld: true };
} } else {
const currentErrors = newPassControl?.errors;
private samePasswordValidation(isSame: boolean, key: string): ValidatorFn { if (currentErrors?.passwordSameAsOld) {
return (control: AbstractControl): ValidationErrors | null => { const { passwordSameAsOld, ...rest } = currentErrors;
const value: string = control.value; newPassControl.setErrors(Object.keys(rest).length ? rest : null);
const keyValue = control.parent?.value[key]; }
return null;
if (isSame) {
return value === keyValue ? {samePassword: true} : null;
} }
return value !== keyValue ? {differencePassword: true} : null;
}; };
} }

34
ui-ngx/src/app/modules/login/login-routing.module.ts

@ -14,8 +14,8 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { NgModule } from '@angular/core'; import { inject, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { ActivatedRouteSnapshot, ResolveFn, Router, RouterModule, RouterStateSnapshot, Routes } from '@angular/router';
import { LoginComponent } from './pages/login/login.component'; import { LoginComponent } from './pages/login/login.component';
import { AuthGuard } from '@core/guards/auth.guard'; import { AuthGuard } from '@core/guards/auth.guard';
@ -26,6 +26,21 @@ import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-fact
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component'; import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component';
import { ForceTwoFactorAuthLoginComponent } from '@modules/login/pages/login/force-two-factor-auth-login.component'; import { ForceTwoFactorAuthLoginComponent } from '@modules/login/pages/login/force-two-factor-auth-login.component';
import { of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AuthService } from '@core/auth/auth.service';
import { UserPasswordPolicy } from '@shared/models/settings.models';
const passwordPolicyResolver: ResolveFn<UserPasswordPolicy> = (route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
router = inject(Router),
authService = inject(AuthService)) => {
return authService.getUserPasswordPolicy({ignoreErrors: true}).pipe(
catchError(() => {
return of({} as UserPasswordPolicy);
})
);
};
const routes: Routes = [ const routes: Routes = [
{ {
@ -53,7 +68,10 @@ const routes: Routes = [
title: 'login.reset-password', title: 'login.reset-password',
module: 'public' module: 'public'
}, },
canActivate: [AuthGuard] canActivate: [AuthGuard],
resolve: {
passwordPolicy: passwordPolicyResolver
}
}, },
{ {
path: 'login/resetExpiredPassword', path: 'login/resetExpiredPassword',
@ -63,7 +81,10 @@ const routes: Routes = [
module: 'public', module: 'public',
expiredPassword: true expiredPassword: true
}, },
canActivate: [AuthGuard] canActivate: [AuthGuard],
resolve: {
passwordPolicy: passwordPolicyResolver
}
}, },
{ {
path: 'login/createPassword', path: 'login/createPassword',
@ -72,7 +93,10 @@ const routes: Routes = [
title: 'login.create-password', title: 'login.create-password',
module: 'public' module: 'public'
}, },
canActivate: [AuthGuard] canActivate: [AuthGuard],
resolve: {
passwordPolicy: passwordPolicyResolver
}
}, },
{ {
path: 'login/mfa', path: 'login/mfa',

22
ui-ngx/src/app/modules/login/pages/login/create-password.component.html

@ -32,15 +32,28 @@
<span style="height: 50px;"></span> <span style="height: 50px;"></span>
<mat-form-field class="mat-block tb-appearance-transparent"> <mat-form-field class="mat-block tb-appearance-transparent">
<mat-label translate>common.password</mat-label> <mat-label translate>common.password</mat-label>
<input matInput type="password" autofocus formControlName="password"/> <input matInput
type="password"
autofocus
cdkOverlayOrigin
#passwordTrigger="cdkOverlayOrigin"
(focus)="passwordTooltip.onFocus()"
(blur)="passwordTooltip.onBlur()"
formControlName="newPassword"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon> <mat-icon class="material-icons" matPrefix>lock</mat-icon>
<tb-toggle-password matSuffix></tb-toggle-password> <tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="this.createPassword.get('newPassword').errors">
{{ 'security.password-requirement.password-not-meet-requirements' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block tb-appearance-transparent"> <mat-form-field class="mat-block tb-appearance-transparent">
<mat-label translate>login.password-again</mat-label> <mat-label translate>login.password-again</mat-label>
<input matInput type="password" formControlName="password2"/> <input matInput type="password" formControlName="newPassword2"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon> <mat-icon class="material-icons" matPrefix>lock</mat-icon>
<tb-toggle-password matSuffix></tb-toggle-password> <tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="createPassword.get('newPassword2').hasError('passwordsNotMatch')">
{{ 'security.password-requirement.new-passwords-not-match' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<div class="flex flex-col items-center justify-start gap-4 gt-xs:flex-row gt-xs:items-start gt-xs:justify-center"> <div class="flex flex-col items-center justify-start gap-4 gt-xs:flex-row gt-xs:items-start gt-xs:justify-center">
<button mat-raised-button color="accent" type="submit" [disabled]="(isLoading$ | async)"> <button mat-raised-button color="accent" type="submit" [disabled]="(isLoading$ | async)">
@ -57,3 +70,8 @@
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
<tb-password-requirements-tooltip #passwordTooltip
[passwordControl]="createPassword.get('newPassword')"
[passwordPolicy]="passwordPolicy"
[trigger]="passwordTrigger">
</tb-password-requirements-tooltip>

60
ui-ngx/src/app/modules/login/pages/login/create-password.component.ts

@ -14,61 +14,55 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { AuthService } from '@core/auth/auth.service'; import { AuthService } from '@core/auth/auth.service';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { UntypedFormBuilder } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs'; import { UserPasswordPolicy } from '@shared/models/settings.models';
import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/models/password.models';
@Component({ @Component({
selector: 'tb-create-password', selector: 'tb-create-password',
templateUrl: './create-password.component.html', templateUrl: './create-password.component.html',
styleUrls: ['./create-password.component.scss'] styleUrls: ['./create-password.component.scss']
}) })
export class CreatePasswordComponent extends PageComponent implements OnInit, OnDestroy { export class CreatePasswordComponent extends PageComponent {
activateToken = ''; passwordPolicy: UserPasswordPolicy;
sub: Subscription; createPassword: FormGroup;
createPassword = this.fb.group({ private activateToken: string;
password: [''],
password2: ['']
});
constructor(protected store: Store<AppState>, constructor(private route: ActivatedRoute,
private route: ActivatedRoute,
private authService: AuthService, private authService: AuthService,
private translate: TranslateService, private fb: FormBuilder) {
public fb: UntypedFormBuilder) { super();
super(store);
} this.activateToken = this.route.snapshot.queryParams['activateToken'] || '';
this.passwordPolicy = this.route.snapshot.data['passwordPolicy'];
ngOnInit() { this.buildCreatePasswordForm();
this.sub = this.route
.queryParams
.subscribe(params => {
this.activateToken = params.activateToken || '';
});
} }
ngOnDestroy(): void { private buildCreatePasswordForm() {
super.ngOnDestroy(); this.createPassword = this.fb.group({
this.sub.unsubscribe(); newPassword: ['', [Validators.required, passwordStrengthValidator(this.passwordPolicy)]],
newPassword2: ['']
}, {
validators: [
passwordsMatchValidator('newPassword', 'newPassword2'),
]
});
} }
onCreatePassword() { onCreatePassword() {
if (this.createPassword.get('password').value !== this.createPassword.get('password2').value) { if (this.createPassword.invalid) {
this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'), this.createPassword.markAllAsTouched();
type: 'error' }));
} else { } else {
this.authService.activate( this.authService.activate(
this.activateToken, this.activateToken,
this.createPassword.get('password').value, true).subscribe(); this.createPassword.get('newPassword').value, true).subscribe();
} }
} }
} }

40
ui-ngx/src/app/modules/login/pages/login/login.component.html

@ -28,19 +28,35 @@
<span style="height: 4px;" *ngIf="!(isLoading$ | async)"></span> <span style="height: 4px;" *ngIf="!(isLoading$ | async)"></span>
<div tb-toast class="layout-padding flex flex-col"> <div tb-toast class="layout-padding flex flex-col">
<span style="height: 50px;"></span> <span style="height: 50px;"></span>
<div class="oauth-container tb-default flex flex-col gap-4" *ngIf="oauth2Clients?.length"> @if(oauth2Clients?.length) {
<ng-container *ngFor="let oauth2Client of oauth2Clients"> <div class="oauth-container tb-default flex flex-col gap-4">
<a mat-raised-button class="login-with-button" href="{{ getOAuth2Uri(oauth2Client) }}"> @if(oauth2Clients.length === 1) {
<mat-icon class="icon" svgIcon="{{ oauth2Client.icon }}"></mat-icon> <a mat-stroked-button class="login-with-button" [href]=getOAuth2Uri(oauth2Clients[0])>
{{ 'login.login-with' | translate: {name: oauth2Client.name} }} <tb-icon matButtonIcon class="tb-mat-20">{{ oauth2Clients[0].icon }}</tb-icon>
</a> {{ 'login.login-with' | translate: {name: oauth2Clients[0].name} }}
</ng-container> </a>
<div class="container-divider"> } @else {
<div class="line"><mat-divider></mat-divider></div> <div class="login-button-container flex flex-wrap justify-center gap-4">
<div class="text mat-typography">{{ "login.or" | translate | uppercase }}</div> @for(oauth2Client of oauth2Clients; track oauth2Client.url) {
<div class="line"><mat-divider></mat-divider></div> <a mat-stroked-button class="login-with-button multiple"
[matTooltip]="('login.login-with' | translate: {name: oauth2Client.name})"
[href]=getOAuth2Uri(oauth2Client)>
@if(oauth2Client.icon) {
<tb-icon class="tb-mat-20">{{ oauth2Client.icon }}</tb-icon>
} @else {
<span>{{ oauth2Client.name }}</span>
}
</a>
}
</div>
}
<div class="container-divider">
<div class="line"><mat-divider></mat-divider></div>
<div class="text">{{ "login.or" | translate | uppercase }}</div>
<div class="line"><mat-divider></mat-divider></div>
</div>
</div> </div>
</div> }
<mat-form-field class="tb-appearance-transparent"> <mat-form-field class="tb-appearance-transparent">
<mat-label translate>login.username</mat-label> <mat-label translate>login.username</mat-label>
<input id="username-input" matInput type="email" autofocus formControlName="username"/> <input id="username-input" matInput type="email" autofocus formControlName="username"/>

36
ui-ngx/src/app/modules/login/pages/login/login.component.scss

@ -59,8 +59,11 @@
} }
.text { .text {
padding-right: 10px; padding-right: 8px;
padding-left: 10px; padding-left: 8px;
font-size: 16px;
line-height: 24px;
letter-spacing: 0.15px;
} }
} }
@ -72,14 +75,33 @@
a.login-with-button { a.login-with-button {
color: rgba(black, 0.87); color: rgba(black, 0.87);
background-color: map-get($tb-dark-theme-background, raised-button); background-color: map-get($tb-dark-theme-background, raised-button);
}
.login-button-container {
a.login-with-button {
--mdc-outlined-button-container-shape: 8px;
max-width: 123px;
min-width: 60px;
flex-grow: 1;
flex-basis: 120px;
.tb-mat-20 {
margin: 0;
vertical-align: text-top;
}
}
&:hover { &:has(> :nth-child(2):last-child) {
border-bottom: 0; a.login-with-button {
max-width: 180px;
flex-basis: 180px;
}
} }
.icon { &:has(> :nth-child(3):last-child) {
height: 20px; a.login-with-button {
width: 20px; max-width: 180px;
}
} }
} }
} }

20
ui-ngx/src/app/modules/login/pages/login/reset-password.component.html

@ -35,15 +35,28 @@
<span style="height: 50px;"></span> <span style="height: 50px;"></span>
<mat-form-field class="mat-block tb-appearance-transparent"> <mat-form-field class="mat-block tb-appearance-transparent">
<mat-label translate>login.new-password</mat-label> <mat-label translate>login.new-password</mat-label>
<input matInput type="password" autofocus formControlName="newPassword"/> <input matInput
type="password"
autofocus
cdkOverlayOrigin
#passwordTrigger="cdkOverlayOrigin"
(focus)="passwordTooltip.onFocus()"
(blur)="passwordTooltip.onBlur()"
formControlName="newPassword"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon> <mat-icon class="material-icons" matPrefix>lock</mat-icon>
<tb-toggle-password matSuffix></tb-toggle-password> <tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="this.resetPassword.get('newPassword').errors">
{{ 'security.password-requirement.password-not-meet-requirements' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block tb-appearance-transparent"> <mat-form-field class="mat-block tb-appearance-transparent">
<mat-label translate>login.new-password-again</mat-label> <mat-label translate>login.new-password-again</mat-label>
<input matInput type="password" formControlName="newPassword2"/> <input matInput type="password" formControlName="newPassword2"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon> <mat-icon class="material-icons" matPrefix>lock</mat-icon>
<tb-toggle-password matSuffix></tb-toggle-password> <tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="resetPassword.get('newPassword2').hasError('passwordsNotMatch')">
{{ 'security.password-requirement.new-passwords-not-match' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<div class="flex flex-col items-center justify-start gap-4 gt-sm:flex-row gt-sm:items-start gt-sm:justify-center"> <div class="flex flex-col items-center justify-start gap-4 gt-sm:flex-row gt-sm:items-start gt-sm:justify-center">
<button mat-raised-button color="accent" type="submit" [disabled]="(isLoading$ | async)"> <button mat-raised-button color="accent" type="submit" [disabled]="(isLoading$ | async)">
@ -60,3 +73,8 @@
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
<tb-password-requirements-tooltip #passwordTooltip
[passwordControl]="resetPassword.get('newPassword')"
[passwordPolicy]="passwordPolicy"
[trigger]="passwordTrigger">
</tb-password-requirements-tooltip>

60
ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts

@ -14,61 +14,55 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { AuthService } from '@core/auth/auth.service'; import { AuthService } from '@core/auth/auth.service';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { UntypedFormBuilder } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { UserPasswordPolicy } from '@shared/models/settings.models';
import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/models/password.models';
@Component({ @Component({
selector: 'tb-reset-password', selector: 'tb-reset-password',
templateUrl: './reset-password.component.html', templateUrl: './reset-password.component.html',
styleUrls: ['./reset-password.component.scss'] styleUrls: ['./reset-password.component.scss']
}) })
export class ResetPasswordComponent extends PageComponent implements OnInit, OnDestroy { export class ResetPasswordComponent extends PageComponent {
isExpiredPassword: boolean; isExpiredPassword: boolean;
resetToken = ''; resetPassword: FormGroup;
sub: Subscription; passwordPolicy: UserPasswordPolicy;
resetPassword = this.fb.group({ private resetToken: string;
newPassword: [''],
newPassword2: ['']
});
constructor(protected store: Store<AppState>, constructor(private route: ActivatedRoute,
private route: ActivatedRoute,
private router: Router, private router: Router,
private authService: AuthService, private authService: AuthService,
private translate: TranslateService, private fb: FormBuilder) {
public fb: UntypedFormBuilder) { super();
super(store);
} this.resetToken = this.route.snapshot.queryParams['resetToken'] || '';
this.passwordPolicy = this.route.snapshot.data['passwordPolicy'];
this.isExpiredPassword = this.route.snapshot.data['expiredPassword'] ?? false;
ngOnInit() { this.buildResetPasswordForm();
this.isExpiredPassword = this.route.snapshot.data.expiredPassword;
this.sub = this.route
.queryParams
.subscribe(params => {
this.resetToken = params.resetToken || '';
});
} }
ngOnDestroy(): void { private buildResetPasswordForm() {
super.ngOnDestroy(); this.resetPassword = this.fb.group({
this.sub.unsubscribe(); newPassword: ['', [Validators.required, passwordStrengthValidator(this.passwordPolicy)]],
newPassword2: ['']
}, {
validators: [
passwordsMatchValidator('newPassword', 'newPassword2'),
]
});
} }
onResetPassword() { onResetPassword() {
if (this.resetPassword.get('newPassword').value !== this.resetPassword.get('newPassword2').value) { if (this.resetPassword.invalid) {
this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'), this.resetPassword.markAllAsTouched();
type: 'error' }));
} else { } else {
this.authService.resetPassword( this.authService.resetPassword(
this.resetToken, this.resetToken,

35
ui-ngx/src/app/shared/components/password-requirements-tooltip.component.html

@ -0,0 +1,35 @@
<!--
Copyright © 2016-2025 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.
-->
<ng-template cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOpen]="isTooltipOpen"
[cdkConnectedOverlayPositions]="overlayPositions">
<div class="password-checklist-card">
@for (rule of passwordErrorRules; track $index) {
@if (!rule.policyProp || passwordPolicy[rule.policyProp] > 0) {
<p class="mat-body flex text-sm">
<tb-icon class="tb-mat-20">
{{ checkForError(rule.key) ? 'mdi:close' : 'mdi:check' }}
</tb-icon>
{{ rule.translation | translate : passwordPolicy }}
</p>
}
}
<div class="tooltip-arrow"></div>
</div>
</ng-template>

48
ui-ngx/src/app/shared/components/password-requirements-tooltip.component.scss

@ -0,0 +1,48 @@
/**
* Copyright © 2016-2025 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.
*/
.password-checklist-card {
background-color: #0000009E;
backdrop-filter: blur(8px);
color: white;
padding: 12px 16px;
border-radius: 8px;
position: relative;
min-width: 220px;
display: flex;
gap: 8px;
flex-direction: column;
& > tb-icon {
color: white;
}
& > p {
margin: 0;
}
& > .tooltip-arrow {
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #002b36;
}
}

53
ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts

@ -0,0 +1,53 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Input, ViewEncapsulation } from '@angular/core';
import { CdkOverlayOrigin, ConnectionPositionPair } from '@angular/cdk/overlay';
import { passwordErrorRules } from '@shared/models/password.models';
import { AbstractControl } from '@angular/forms';
import { UserPasswordPolicy } from '@shared/models/settings.models';
import { POSITION_MAP } from '@shared/models/overlay.models';
@Component({
selector: 'tb-password-requirements-tooltip',
templateUrl: './password-requirements-tooltip.component.html',
styleUrl: './password-requirements-tooltip.component.scss',
encapsulation: ViewEncapsulation.None
})
export class PasswordRequirementsTooltipComponent {
@Input() passwordControl: AbstractControl;
@Input() passwordPolicy: UserPasswordPolicy;
@Input() trigger: CdkOverlayOrigin;
passwordErrorRules = passwordErrorRules;
isTooltipOpen = false;
overlayPositions: ConnectionPositionPair[] = [
{...POSITION_MAP.top, offsetY: -20}
];
checkForError(errorName: string): boolean {
return this.passwordControl?.hasError(errorName) ?? false;
}
onFocus(): void {
this.isTooltipOpen = true;
}
onBlur(): void {
this.isTooltipOpen = false;
}
}

2
ui-ngx/src/app/shared/models/api-key.models.ts

@ -19,7 +19,7 @@ import { HasTenantId } from '@shared/models/entity.models';
import { ApiKeyId } from '@shared/models/id/api-key-id'; import { ApiKeyId } from '@shared/models/id/api-key-id';
import { UserId } from '@shared/models/id/user-id'; import { UserId } from '@shared/models/id/user-id';
export const userInfoCommand = (key: string): string => `curl -X GET "${window.location.origin}/api/auth/user" -H "Content-Type: application/json" -H "X-Authorization: ApiKey ${key}"` export const userInfoCommand = (baseUrl: string, apiKey: string): string => `curl -X GET "${baseUrl}/api/auth/user" -H "Content-Type: application/json" -H "X-Authorization: ApiKey ${apiKey}"`
export interface ApiKeyInfo extends BaseData<ApiKeyId>, HasTenantId { export interface ApiKeyInfo extends BaseData<ApiKeyId>, HasTenantId {
enabled: boolean; enabled: boolean;

1
ui-ngx/src/app/shared/models/calculated-field.models.ts

@ -233,6 +233,7 @@ export type AttributeOutputStrategy =
export interface AttributeImmediateOutputStrategy { export interface AttributeImmediateOutputStrategy {
type: OutputStrategyType.IMMEDIATE; type: OutputStrategyType.IMMEDIATE;
updateAttributesOnlyOnValueChange: boolean; updateAttributesOnlyOnValueChange: boolean;
sendAttributesUpdatedNotification: boolean;
saveAttribute: boolean; saveAttribute: boolean;
sendWsUpdate: boolean; sendWsUpdate: boolean;
processCfs: boolean; processCfs: boolean;

106
ui-ngx/src/app/shared/models/password.models.ts

@ -0,0 +1,106 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { UserPasswordPolicy } from '@shared/models/settings.models';
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
import { isEqual } from '@core/utils';
export enum TooltipPasswordErrorMessageKey {
minLength = 'security.password-requirement.password-tooltip-min-length',
maxLength = 'security.password-requirement.password-tooltip-max-length',
notUpperCase = 'security.password-requirement.password-tooltip-uppercase',
notLowerCase = 'security.password-requirement.password-tooltip-lowercase',
notNumeric = 'security.password-requirement.password-tooltip-digit',
notSpecial = 'security.password-requirement.password-tooltip-special-characters',
hasWhitespaces = 'security.password-requirement.password-should-not-contain-spaces'
}
export const passwordErrorRules = [
{ key: 'minLength', policyProp: 'minimumLength', translation: TooltipPasswordErrorMessageKey.minLength },
{ key: 'notUpperCase', policyProp: 'minimumUppercaseLetters', translation: TooltipPasswordErrorMessageKey.notUpperCase },
{ key: 'notLowerCase', policyProp: 'minimumLowercaseLetters', translation: TooltipPasswordErrorMessageKey.notLowerCase },
{ key: 'notNumeric', policyProp: 'minimumDigits', translation: TooltipPasswordErrorMessageKey.notNumeric },
{ key: 'notSpecial', policyProp: 'minimumSpecialCharacters', translation: TooltipPasswordErrorMessageKey.notSpecial },
{ key: 'maxLength', policyProp: 'maximumLength', translation: TooltipPasswordErrorMessageKey.maxLength },
{ key: 'hasWhitespaces', policyProp: 'hasWhitespaces', translation: TooltipPasswordErrorMessageKey.hasWhitespaces },
];
export const passwordsMatchValidator = (firstControlName: string, secondControlName: string): ValidatorFn =>{
return (group: AbstractControl): ValidationErrors | null => {
const newPassControl = group.get(firstControlName);
const confirmControl = group.get(secondControlName);
if (!newPassControl || !confirmControl) {
return null;
}
const newPass = newPassControl.value ?? '';
const confirm = confirmControl.value ?? '';
if ((newPass || confirm) && confirm !== newPass) {
confirmControl.setErrors({ passwordsNotMatch: true });
return { passwordsNotMatch: true };
} else {
const currentErrors = confirmControl?.errors;
if (currentErrors?.['passwordsNotMatch']) {
const { passwordsNotMatch, ...rest } = currentErrors;
confirmControl?.setErrors(Object.keys(rest).length ? rest : null);
}
return null;
}
};
}
export const passwordStrengthValidator = (passwordPolicy: UserPasswordPolicy): ValidatorFn => {
return (control: AbstractControl): ValidationErrors | null => {
const value: string = control.value;
const errors: any = {};
if (passwordPolicy.minimumUppercaseLetters > 0 &&
!new RegExp(`(?:.*?[A-Z]){${passwordPolicy.minimumUppercaseLetters}}`).test(value)) {
errors.notUpperCase = true;
}
if (passwordPolicy.minimumLowercaseLetters > 0 &&
!new RegExp(`(?:.*?[a-z]){${passwordPolicy.minimumLowercaseLetters}}`).test(value)) {
errors.notLowerCase = true;
}
if (passwordPolicy.minimumDigits > 0
&& !new RegExp(`(?:.*?\\d){${passwordPolicy.minimumDigits}}`).test(value)) {
errors.notNumeric = true;
}
if (passwordPolicy.minimumSpecialCharacters > 0 &&
!new RegExp(`(?:.*?[\\W_]){${passwordPolicy.minimumSpecialCharacters}}`).test(value)) {
errors.notSpecial = true;
}
if (!passwordPolicy.allowWhitespaces && /\s/.test(value)) {
errors.hasWhitespaces = true;
}
if (passwordPolicy.minimumLength > 0 && value.length < passwordPolicy.minimumLength) {
errors.minLength = true;
}
if (!value.length || passwordPolicy.maximumLength > 0 && value.length > passwordPolicy.maximumLength) {
errors.maxLength = true;
}
return isEqual(errors, {}) ? null : errors;
};
}

1
ui-ngx/src/app/shared/models/public-api.ts

@ -71,3 +71,4 @@ export * from './query/query.models';
export * from './regex.constants'; export * from './regex.constants';
export * from './trendz-settings.models'; export * from './trendz-settings.models';
export * from './ai-model.models'; export * from './ai-model.models';
export * from './password.models';

5
ui-ngx/src/app/shared/models/time/time.models.ts

@ -315,8 +315,9 @@ const getTimewindowType = (timewindow: Timewindow): TimewindowType => {
}; };
export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalOnly: boolean, export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalOnly: boolean,
historyOnly: boolean, timeService: TimeService, hasAggregation: boolean): Timewindow => { historyOnly: boolean, timeService: TimeService, hasAggregation: boolean,
const model = defaultTimewindow(timeService); isDashboard = false): Timewindow => {
const model = defaultTimewindow(timeService, isDashboard);
if (value) { if (value) {
if (value.allowedAggTypes?.length) { if (value.allowedAggTypes?.length) {
model.allowedAggTypes = value.allowedAggTypes; model.allowedAggTypes = value.allowedAggTypes;

11
ui-ngx/src/app/shared/shared.module.ts

@ -220,14 +220,19 @@ import { CountryData } from '@shared/models/country.models';
import { SvgXmlComponent } from '@shared/components/svg-xml.component'; import { SvgXmlComponent } from '@shared/components/svg-xml.component';
import { DatapointsLimitComponent } from '@shared/components/time/datapoints-limit.component'; import { DatapointsLimitComponent } from '@shared/components/time/datapoints-limit.component';
import { AggregationTypeSelectComponent } from '@shared/components/time/aggregation/aggregation-type-select.component'; import { AggregationTypeSelectComponent } from '@shared/components/time/aggregation/aggregation-type-select.component';
import { AggregationOptionsConfigPanelComponent } from '@shared/components/time/aggregation/aggregation-options-config-panel.component'; import {
AggregationOptionsConfigPanelComponent
} from '@shared/components/time/aggregation/aggregation-options-config-panel.component';
import { IntervalOptionsConfigPanelComponent } from '@shared/components/time/interval-options-config-panel.component'; import { IntervalOptionsConfigPanelComponent } from '@shared/components/time/interval-options-config-panel.component';
import { GroupingIntervalOptionsComponent } from '@shared/components/time/aggregation/grouping-interval-options.component'; import {
GroupingIntervalOptionsComponent
} from '@shared/components/time/aggregation/grouping-interval-options.component';
import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component'; import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component';
import { JsFuncModuleRowComponent } from '@shared/components/js-func-module-row.component'; import { JsFuncModuleRowComponent } from '@shared/components/js-func-module-row.component';
import { EntityKeyAutocompleteComponent } from '@shared/components/entity/entity-key-autocomplete.component'; import { EntityKeyAutocompleteComponent } from '@shared/components/entity/entity-key-autocomplete.component';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { MqttVersionSelectComponent } from '@shared/components/mqtt-version-select.component'; import { MqttVersionSelectComponent } from '@shared/components/mqtt-version-select.component';
import { PasswordRequirementsTooltipComponent } from '@shared/components/password-requirements-tooltip.component';
import { StringPatternAutocompleteComponent } from '@shared/components/string-pattern-autocomplete.component'; import { StringPatternAutocompleteComponent } from '@shared/components/string-pattern-autocomplete.component';
import { TimeUnitInputComponent } from '@shared/components/time-unit-input.component'; import { TimeUnitInputComponent } from '@shared/components/time-unit-input.component';
import { DateExpirationPipe } from '@shared/pipe/date-expiration.pipe'; import { DateExpirationPipe } from '@shared/pipe/date-expiration.pipe';
@ -448,6 +453,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ScadaSymbolInputComponent, ScadaSymbolInputComponent,
EntityKeyAutocompleteComponent, EntityKeyAutocompleteComponent,
MqttVersionSelectComponent, MqttVersionSelectComponent,
PasswordRequirementsTooltipComponent,
TimeUnitInputComponent, TimeUnitInputComponent,
StringPatternAutocompleteComponent StringPatternAutocompleteComponent
], ],
@ -715,6 +721,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ScadaSymbolInputComponent, ScadaSymbolInputComponent,
EntityKeyAutocompleteComponent, EntityKeyAutocompleteComponent,
MqttVersionSelectComponent, MqttVersionSelectComponent,
PasswordRequirementsTooltipComponent,
TimeUnitInputComponent, TimeUnitInputComponent,
StringPatternAutocompleteComponent StringPatternAutocompleteComponent
] ]

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

@ -772,6 +772,7 @@
"name-max-length": "Name should be less than 256", "name-max-length": "Name should be less than 256",
"label-max-length": "Label should be less than 256", "label-max-length": "Label should be less than 256",
"description": "Description", "description": "Description",
"description-required": "Description is required.",
"type": "Type", "type": "Type",
"type-required": "Type is required.", "type-required": "Type is required.",
"details": "Details", "details": "Details",
@ -1011,6 +1012,7 @@
"generated-api-key-title": "API key generated. Let’s check connectivity!", "generated-api-key-title": "API key generated. Let’s check connectivity!",
"generated-api-key-copy": "Make sure to copy and save your API key now as you will not be able to see it again.", "generated-api-key-copy": "Make sure to copy and save your API key now as you will not be able to see it again.",
"generated-api-key-command": "Use the following instructions to check connectivity. As a result, you should receive the current user information:", "generated-api-key-command": "Use the following instructions to check connectivity. As a result, you should receive the current user information:",
"generated-api-key-insecure-url": "Executing commands over an insecure HTTP connection will send your API key unencrypted, making it vulnerable to interception.",
"list": "{ count, plural, =1 {One API key} other {List of # API keys} }", "list": "{ count, plural, =1 {One API key} other {List of # API keys} }",
"manage": "Manage", "manage": "Manage",
"manage-api-keys": "Manage API keys", "manage-api-keys": "Manage API keys",
@ -1257,20 +1259,25 @@
"save-latest-values": "Save to latest values", "save-latest-values": "Save to latest values",
"send-web-sockets": "Send to WebSockets", "send-web-sockets": "Send to WebSockets",
"save-calculated-fields": "Send to Calculated fields", "save-calculated-fields": "Send to Calculated fields",
"update-attributes-only-on-value-change": "Save attributes only if the value changes", "update-attribute-only-on-value-change": "Update attribute only on value change",
"send-attributes-updated-notification": "Send attributes updated notification",
"ttl": "Custom TTL", "ttl": "Custom TTL",
"ttl-required": "TTL is required", "ttl-required": "TTL is required",
"ttl-min": "Only 0 minimum TTL is allowed", "ttl-min": "Only 0 minimum TTL is allowed",
"processing-parameters": "Processing parameters",
"hint": { "hint": {
"strategy": "Controls whether the result is processed immediately or sent to a rule chain for additional processing", "strategy": "Controls whether the result is processed immediately or sent to a rule chain for additional processing",
"processing-options": "Processing options", "processing-options": "Processing options",
"update-attributes-only-on-value-change": "Updates the attributes on every incoming message disregarding if their value has changed. Increases API usage and reduces performance.", "update-attribute-only-on-value-change": "Updates attribute on every incoming message, regardless of whether the value has changed. This increases API usage and reduces performance.",
"update-attributes-only-on-value-change-enabled": "Updates the attributes only if their value has changed. If the value is not changed, no update to the attribute timestamp nor attribute change notification will be sent.", "update-attribute-only-on-value-change-enabled": "Updates attribute only when the value changes. If the value is unchanged, timestamps are not updated and notifications are not sent.",
"send-attributes-updated-notification": "Sends an Attributes Updated event to the default rule chain.",
"save-time-series": "Saves time series data to the ts_kv table in the database.", "save-time-series": "Saves time series data to the ts_kv table in the database.",
"save-database": "Saves attribute data to the database.", "save-database": "Saves attribute data to the database.",
"save-latest-values": "Updates time series data in the ts_kv_latest table in the database if the new value is more recent.", "save-latest-values": "Updates time series data in the ts_kv_latest table in the database if the new value is more recent.",
"send-web-sockets": "Notifies WebSocket subscriptions about updates to the attribute data.", "send-web-sockets-attribute": "Notifies WebSocket subscriptions about updates to the attribute data.",
"save-calculated-fields": "Notifies calculated fields about updates to the attribute data.", "send-web-sockets-time-series": "Notifies WebSocket subscriptions about updates to the time series data.",
"save-calculated-fields-attribute": "Notifies calculated fields about updates to the attribute data.",
"save-calculated-fields-time-series": "Notifies calculated fields about updates to the time series data.",
"ttl": "Defines the retention period for time series data. If disabled, the Tenant Profile TTL is used." "ttl": "Defines the retention period for time series data. If disabled, the Tenant Profile TTL is used."
} }
}, },
@ -4891,6 +4898,12 @@
"at-least": "At least:", "at-least": "At least:",
"character": "{ count, plural, =1 {1 character} other {# characters} }", "character": "{ count, plural, =1 {1 character} other {# characters} }",
"digit": "{ count, plural, =1 {1 digit} other {# digits} }", "digit": "{ count, plural, =1 {1 digit} other {# digits} }",
"password-tooltip-min-length": "At least {{minimumLength}} characters long",
"password-tooltip-max-length": "At most {{maximumLength}} characters long",
"password-tooltip-uppercase": "{{minimumUppercaseLetters}} uppercase character",
"password-tooltip-lowercase": "{{minimumLowercaseLetters}} lowercase character",
"password-tooltip-digit": "{{minimumDigits}} number",
"password-tooltip-special-characters": "{{minimumSpecialCharacters}} special character",
"incorrect-password-try-again": "Incorrect password. Try again", "incorrect-password-try-again": "Incorrect password. Try again",
"lowercase-letter": "{ count, plural, =1 {1 lowercase letter} other {# lowercase letters} }", "lowercase-letter": "{ count, plural, =1 {1 lowercase letter} other {# lowercase letters} }",
"new-passwords-not-match": "New password didn't match", "new-passwords-not-match": "New password didn't match",

Loading…
Cancel
Save