From fcc2a917fde477467e54e0ecb4ea74ed016479a6 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Tue, 23 Sep 2025 12:00:16 +0300 Subject: [PATCH 01/46] Added User sync for Edge --- .../service/edge/EdgeContextComponent.java | 4 + .../service/edge/rpc/EdgeGrpcSession.java | 24 +++ .../rpc/processor/user/BaseUserProcessor.java | 130 ++++++++++++ .../rpc/processor/user/UserEdgeProcessor.java | 97 ++++++++- .../rpc/processor/user/UserProcessor.java | 31 +++ .../thingsboard/server/edge/UserEdgeTest.java | 193 ++++++++++++------ .../server/dao/user/UserService.java | 7 + common/edge-api/src/main/proto/edge.proto | 2 + .../service/validator/UserDataValidator.java | 1 + .../server/dao/user/UserServiceImpl.java | 77 +++++-- 10 files changed, 485 insertions(+), 81 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserProcessor.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 5c5eea32fd..0f469b15b8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -73,6 +73,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.RuleChainMetadataEdgeProcessor; 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.executors.GrpcCallbackExecutorService; @@ -261,6 +262,9 @@ public class EdgeContextComponent { @Autowired private CalculatedFieldProcessor calculatedFieldProcessor; + @Autowired + private UserProcessor userProcessor; + public EdgeProcessor getProcessor(EdgeEventType edgeEventType) { EdgeProcessor processor = processorMap.get(edgeEventType); if (processor == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index a86409c6af..393d1ba3da 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -83,6 +83,8 @@ import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; 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.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -100,6 +102,7 @@ import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiConsumer; @@ -121,6 +124,7 @@ public abstract class EdgeGrpcSession implements Closeable { private final EdgeSessionState sessionState = new EdgeSessionState(); private final ReentrantLock downlinkMsgLock = new ReentrantLock(); + private final Lock sequenceDependencyLock = new ReentrantLock(); protected EdgeContextComponent ctx; protected Edge edge; @@ -934,6 +938,26 @@ public abstract class EdgeGrpcSession implements Closeable { result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg)); } } + 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) { 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); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java new file mode 100644 index 0000000000..92d5664a4f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -0,0 +1,130 @@ +/** + * 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.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserCredentials; +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 userValidator; + + protected Pair 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 RuntimeException("[{" + tenantId + "}] userUpdateMsg {" + userUpdateMsg + "} cannot be converted to user"); + } + + 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 updateUserCredentials(TenantId tenantId, UserCredentialsUpdateMsg updateMsg) { + UserCredentials userCredentialsFromUpdateMsg = JacksonUtil.fromString(updateMsg.getEntity(), UserCredentials.class, true); + if (userCredentialsFromUpdateMsg == null) { + throw new RuntimeException(String.format("[%s] Failed to parse UserCredentials from updateMsg: %s", tenantId, updateMsg)); + } + + User user = edgeCtx.getUserService().findUserById(tenantId, userCredentialsFromUpdateMsg.getUserId()); + if (user == null) { + log.warn("[{}] Can't find user by id [{}] skipping credentials update. UserCredentialsUpdateMsg [{}]", + tenantId, userCredentialsFromUpdateMsg.getUserId(), updateMsg); + return; + } + + log.debug("[{}] Updating user credentials for user [{}]. New credentials Id [{}], enabled [{}]", + tenantId, user.getName(), userCredentialsFromUpdateMsg.getId(), userCredentialsFromUpdateMsg.isEnabled()); + + try { + UserCredentials existing = edgeCtx.getUserService().findUserCredentialsByUserId(tenantId, user.getId()); + boolean created = existing == null; + + UserCredentials updated = created ? new UserCredentials() : existing; + updated.setId(userCredentialsFromUpdateMsg.getId()); + updated.setUserId(user.getId()); + updated.setEnabled(userCredentialsFromUpdateMsg.isEnabled()); + updated.setActivateToken(userCredentialsFromUpdateMsg.getActivateToken()); + updated.setAdditionalInfo(userCredentialsFromUpdateMsg.getAdditionalInfo()); + updated.setPassword(userCredentialsFromUpdateMsg.getPassword()); + updated.setResetToken(userCredentialsFromUpdateMsg.getResetToken()); + + + if (created) { + edgeCtx.getUserService().saveUserCredentials(tenantId, updated, false); + } else { + edgeCtx.getUserService().replaceUserCredentials(tenantId, updated, existing.getId(), false); + } + } catch (Exception e) { + log.error("[{}] Can't update user credentials for user [{}], userCredentialsUpdateMsg [{}]", + tenantId, user.getName(), updateMsg, e); + throw new RuntimeException(e); + } + + } + + protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, User user, UserUpdateMsg userUpdateMsg); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index fdd03d63f3..e0bd07c6c6 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -15,26 +15,106 @@ */ 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 org.springframework.data.util.Pair; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; 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.EdgeEventActionType; 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.msg.TbMsgType; 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.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; 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.service.edge.EdgeMsgConstructorUtils; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; @Slf4j @Component @TbCoreComponent -public class UserEdgeProcessor extends BaseEdgeProcessor { +public class UserEdgeProcessor extends BaseUserProcessor implements UserProcessor { + + @Override + public ListenableFuture 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); + } + 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 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 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 public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { @@ -48,7 +128,7 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addUserUpdateMsg(EdgeMsgConstructorUtils.constructUserUpdatedMsg(msgType, user)); UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); - if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { + if (userCredentialsByUserId != null) { builder.addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId)); } return builder.build(); @@ -62,11 +142,10 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { } case CREDENTIALS_UPDATED -> { UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); - if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { - UserCredentialsUpdateMsg userCredentialsUpdateMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId); + if (userCredentialsByUserId != null) { return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addUserCredentialsUpdateMsg(userCredentialsUpdateMsg) + .addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId)) .build(); } } @@ -79,4 +158,10 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { 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); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserProcessor.java new file mode 100644 index 0000000000..dd9659c1f4 --- /dev/null +++ b/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 processUserMsgFromEdge(TenantId tenantId, Edge edge, UserUpdateMsg userUpdateMsg); + + ListenableFuture processUserCredentialsMsgFromEdge(TenantId tenantId, Edge edge, UserCredentialsUpdateMsg userCredentialsUpdateMsg); + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index 05debdc015..84442e2d0a 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.edge; +import com.fasterxml.jackson.databind.JsonNode; import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; @@ -22,8 +23,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; +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.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.UserCredentials; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -32,11 +37,14 @@ import org.thingsboard.server.gen.edge.v1.UplinkMsg; 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.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.security.model.ChangePasswordRequest; import java.util.Optional; +import java.util.UUID; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.dao.user.UserServiceImpl.DEFAULT_TOKEN_LENGTH; @DaoSqlTest public class UserEdgeTest extends AbstractEdgeTest { @@ -44,46 +52,35 @@ public class UserEdgeTest extends AbstractEdgeTest { @Autowired private BCryptPasswordEncoder passwordEncoder; + private static final String DEFAULT_FIRST_NAME = "Boris"; + private static final String UPDATED_LAST_NAME = "Borisov"; + @Test public void testCreateUpdateDeleteTenantUser() throws Exception { // create user edgeImitator.expectMessageAmount(3); - User newTenantAdmin = new User(); - newTenantAdmin.setAuthority(Authority.TENANT_ADMIN); - newTenantAdmin.setTenantId(tenantId); - newTenantAdmin.setEmail("tenantAdmin@thingsboard.org"); - newTenantAdmin.setFirstName("Boris"); - newTenantAdmin.setLastName("Johnson"); + User newTenantAdmin = buildUser(Authority.TENANT_ADMIN, null, "tenantAdmin@thingsboard.org", DEFAULT_FIRST_NAME, "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 userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); - Assert.assertTrue(userUpdateMsgOpt.isPresent()); - UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); + + UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg(); 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 userCredentialsUpdateMsgOpt = edgeImitator.findMessageByType(UserCredentialsUpdateMsg.class); - Assert.assertTrue(userCredentialsUpdateMsgOpt.isPresent()); // update user edgeImitator.expectMessageAmount(2); - savedTenantAdmin.setLastName("Borisov"); + savedTenantAdmin.setLastName(UPDATED_LAST_NAME); 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); + + userUpdateMsg = getLatestUserUpdateMsg(); + User userFromMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); + Assert.assertNotNull(userFromMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedTenantAdmin.getLastName(), userMsg.getLastName()); + Assert.assertEquals(UPDATED_LAST_NAME, userFromMsg.getLastName()); // update user credentials login(savedTenantAdmin.getEmail(), "tenant"); @@ -94,6 +91,7 @@ public class UserEdgeTest extends AbstractEdgeTest { changePasswordRequest.setNewPassword("newTenant"); doPost("/api/auth/changePassword", changePasswordRequest); Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; @@ -109,6 +107,7 @@ public class UserEdgeTest extends AbstractEdgeTest { doDelete("/api/user/" + savedTenantAdmin.getUuidId()) .andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserUpdateMsg); userUpdateMsg = (UserUpdateMsg) latestMessage; @@ -120,27 +119,11 @@ public class UserEdgeTest extends AbstractEdgeTest { @Test public void testCreateUpdateDeleteCustomerUser() throws Exception { // create customer - edgeImitator.expectMessageAmount(1); - Customer customer = new Customer(); - customer.setTitle("Edge Customer"); - Customer savedCustomer = doPost("/api/customer", customer, Customer.class); - Assert.assertFalse(edgeImitator.waitForMessages(5)); - - // assign edge to customer - edgeImitator.expectMessageAmount(2); - doPost("/api/customer/" + savedCustomer.getUuidId() - + "/edge/" + edge.getUuidId(), Edge.class); - Assert.assertTrue(edgeImitator.waitForMessages()); + Customer savedCustomer = createAndAssignCustomerToEdge("Edge Customer"); // create user edgeImitator.expectMessageAmount(3); - User customerUser = new User(); - customerUser.setAuthority(Authority.CUSTOMER_USER); - customerUser.setTenantId(tenantId); - customerUser.setCustomerId(savedCustomer.getId()); - customerUser.setEmail("customerUser@thingsboard.org"); - customerUser.setFirstName("John"); - customerUser.setLastName("Edwards"); + User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId(), "customerUser@thingsboard.org", "John", "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()); @@ -205,32 +188,57 @@ public class UserEdgeTest extends AbstractEdgeTest { } @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()); + public void testSendUserToCloudFromEdge() throws Exception { + // create customer + Customer savedCustomer = createAndAssignCustomerToEdge("Edge Customer"); - testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + // create user + User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId(), "customerUser@thingsboard.org", DEFAULT_FIRST_NAME, "Johnson"); + + UUID uuid = UUID.randomUUID(); + customerUser.setId(new UserId(uuid)); + UUID userCredentialsUuid = UUID.randomUUID(); + UplinkMsg uplinkMsg = constructUserUplinkMsg(customerUser, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userCredentialsUuid); edgeImitator.expectResponsesAmount(1); - edgeImitator.expectMessageAmount(1); - edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + edgeImitator.sendUplinkMsg(uplinkMsg); Assert.assertTrue(edgeImitator.waitForResponses()); - 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(tenantAdminUserId, userCredentialsMsg.getUserId()); + User userFromCloud = doGet("/api/user/" + uuid, User.class); + Assert.assertNotNull(userFromCloud); + Assert.assertEquals(customerUser.getEmail(), userFromCloud.getEmail()); + //check user with existing email + User userWithExistingEmail = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId(), "customerUser@thingsboard.org", DEFAULT_FIRST_NAME, "Johnson"); + + UUID uuidForExistingEmail = UUID.randomUUID(); + userWithExistingEmail.setId(new UserId(uuidForExistingEmail)); + UUID userCredentialsUuidForExistingEmail = UUID.randomUUID(); + UplinkMsg uplinkMsgForExistingEmail = constructUserUplinkMsg(userWithExistingEmail, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userCredentialsUuidForExistingEmail); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgForExistingEmail); + Assert.assertTrue(edgeImitator.waitForResponses()); + + User userFromCloudWithExistingEmail = doGet("/api/user/" + uuidForExistingEmail, User.class); + Assert.assertNotNull(userFromCloudWithExistingEmail); + Assert.assertNotEquals(userWithExistingEmail.getEmail(), userFromCloudWithExistingEmail.getEmail()); + + assertUserCredentialsFlags(userFromCloud, false, false); + + UplinkMsg enabledCredentialsUplinkMsg = constructUserCredentialsUplinkMsg(customerUser.getId(), "password", true, userCredentialsUuid); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(enabledCredentialsUplinkMsg); + Assert.assertTrue(edgeImitator.waitForResponses()); + + User cloudUserWithCredentials = doGet("/api/user/" + uuid, User.class); + Assert.assertNotNull(cloudUserWithCredentials); + + assertUserCredentialsFlags(cloudUserWithCredentials, true, true); } @Test - public void sendUserCredentialsRequest() throws Exception { + public void testSendUserCredentialsRequest() throws Exception { UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder(); userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits()); @@ -256,4 +264,71 @@ public class UserEdgeTest extends AbstractEdgeTest { testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); } + private User buildUser(Authority authority, CustomerId customerId, String email, String firstName, String lastName) { + User customerUser = new User(); + customerUser.setAuthority(authority); + customerUser.setTenantId(tenantId); + customerUser.setCustomerId(customerId); + customerUser.setEmail(email); + customerUser.setFirstName(firstName); + customerUser.setLastName(lastName); + return customerUser; + } + + private Customer createAndAssignCustomerToEdge(String title) throws Exception { + edgeImitator.expectMessageAmount(1); + Customer customer = new Customer(); + customer.setTitle(title); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + Assert.assertFalse(edgeImitator.waitForMessages(5)); + + edgeImitator.expectMessageAmount(2); + doPost("/api/customer/" + savedCustomer.getUuidId() + "/edge/" + edge.getUuidId(), Edge.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + return savedCustomer; + } + + private UplinkMsg constructUserUplinkMsg(User user, UpdateMsgType msgType, UUID userCredentialsUuid) { + UserUpdateMsg userUpdateMsg = EdgeMsgConstructorUtils.constructUserUpdatedMsg(msgType, user); + + UserCredentials userCredentials = new UserCredentials(); + userCredentials.setId(new UserCredentialsId(userCredentialsUuid)); + userCredentials.setUserId(user.getId()); + userCredentials.setEnabled(false); + userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode()); + userCredentials.setActivateToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH)); + UserCredentialsUpdateMsg userCredentialsMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentials); + + return UplinkMsg.newBuilder() + .addUserUpdateMsg(userUpdateMsg) + .addUserCredentialsUpdateMsg(userCredentialsMsg) + .build(); + } + + private UplinkMsg constructUserCredentialsUplinkMsg(UserId userId, String password, boolean enabled, UUID userCredentialsUuid) { + UserCredentials userCredentials = new UserCredentials(); + userCredentials.setId(new UserCredentialsId(userCredentialsUuid)); + userCredentials.setUserId(userId); + userCredentials.setEnabled(enabled); + userCredentials.setPassword(password); + UserCredentialsUpdateMsg credsMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentials); + return UplinkMsg.newBuilder() + .addUserCredentialsUpdateMsg(credsMsg) + .build(); + } + + private void assertUserCredentialsFlags(User user, boolean enabled, boolean activated) { + JsonNode info = user.getAdditionalInfo(); + Assert.assertNotNull(info); + Assert.assertEquals(enabled, info.get("userCredentialsEnabled").asBoolean()); + Assert.assertEquals(activated, info.get("userActivated").asBoolean()); + } + + private UserUpdateMsg getLatestUserUpdateMsg() { + Optional opt = edgeImitator.findMessageByType(UserUpdateMsg.class); + Assert.assertTrue(opt.isPresent()); + return opt.get(); + } + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index c016631064..1c8943ad7a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -45,6 +45,8 @@ public interface UserService extends EntityDaoService { User saveUser(TenantId tenantId, User user); + User saveUser(TenantId tenantId, User user, boolean doValidate); + UserCredentials findUserCredentialsByUserId(TenantId tenantId, UserId userId); UserCredentials findUserCredentialsByActivateToken(TenantId tenantId, String activateToken); @@ -53,6 +55,8 @@ public interface UserService extends EntityDaoService { UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials); + UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials, boolean doValidate); + UserCredentials activateUserCredentials(TenantId tenantId, String activateToken, String password); UserCredentials requestPasswordReset(TenantId tenantId, String email); @@ -67,6 +71,9 @@ public interface UserService extends EntityDaoService { UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials); + UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials, + UserCredentialsId oldUserCredentialsId, boolean doValidate); + void deleteUser(TenantId tenantId, User user); PageData findUsersByTenantId(TenantId tenantId, PageLink pageLink); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index dbda462a99..7eddb326ef 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -441,6 +441,8 @@ message UplinkMsg { repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24; repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25; repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26; + repeated UserUpdateMsg userUpdateMsg = 27; + repeated UserCredentialsUpdateMsg userCredentialsUpdateMsg = 28; } message UplinkResponseMsg { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java index 777ce4dfc8..64d0fc21b8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java @@ -136,4 +136,5 @@ public class UserDataValidator extends DataValidator { } } } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 5c94ba1891..8c229c815d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -70,6 +70,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.thingsboard.server.common.data.StringUtils.generateSafeToken; @@ -84,7 +85,7 @@ public class UserServiceImpl extends AbstractCachedEntityService tenantId); + if (doValidate) { + userCredentialsValidator.validate(userCredentials, data -> tenantId); + } UserCredentials result = userCredentialsDao.save(tenantId, userCredentials); eventPublisher.publishEvent(ActionEntityEvent.builder() .tenantId(tenantId) @@ -304,19 +324,44 @@ public class UserServiceImpl extends AbstractCachedEntityService tenantId); - userCredentialsDao.removeById(tenantId, userCredentials.getUuidId()); - userCredentials.setId(null); - if (userCredentials.getPassword() != null) { - updatePasswordHistory(userCredentials); + return replaceUserCredentialsInternal(tenantId, userCredentials, userCredentials.getUuidId(), true); + } + + @Override + public UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials, + UserCredentialsId oldUserCredentialsId, boolean doValidate) { + return replaceUserCredentialsInternal(tenantId, userCredentials, oldUserCredentialsId.getId(), doValidate); + } + + private UserCredentials replaceUserCredentialsInternal(TenantId tenantId, UserCredentials userCredentials, + UUID oldCredentialsUuid, boolean doValidate) { + log.trace("[{}] Replacing user credentials for user [{}], old credentials ID [{}]", + tenantId, userCredentials.getUserId(), oldCredentialsUuid); + + if (doValidate) { + userCredentialsValidator.validate(userCredentials, data -> tenantId); + } + + try { + userCredentialsDao.removeById(tenantId, oldCredentialsUuid); + + if (userCredentials.getPassword() != null) { + updatePasswordHistory(userCredentials); + } + + UserCredentials savedCredentials = userCredentialsDao.save(tenantId, userCredentials); + + eventPublisher.publishEvent(ActionEntityEvent.builder() + .tenantId(tenantId) + .entityId(userCredentials.getUserId()) + .actionType(ActionType.CREDENTIALS_UPDATED) + .build()); + + return savedCredentials; + } catch (Exception e) { + log.error("[{}] Failed to replace user credentials for user [{}]", tenantId, userCredentials.getUserId(), e); + throw new RuntimeException("Failed to replace user credentials", e); } - UserCredentials result = userCredentialsDao.save(tenantId, userCredentials); - eventPublisher.publishEvent(ActionEntityEvent.builder() - .tenantId(tenantId) - .entityId(userCredentials.getUserId()) - .actionType(ActionType.CREDENTIALS_UPDATED).build()); - return result; } @Override From c1b5febc754a4da4bfb14c496d7cb87ead67b709 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Wed, 8 Oct 2025 14:53:10 +0300 Subject: [PATCH 02/46] Refactor --- .../rpc/processor/user/BaseUserProcessor.java | 13 +- .../rpc/processor/user/UserEdgeProcessor.java | 2 +- .../thingsboard/server/edge/UserEdgeTest.java | 311 +++++++++--------- .../service/validator/UserDataValidator.java | 1 - .../server/dao/user/UserServiceImpl.java | 3 +- 5 files changed, 153 insertions(+), 177 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java index 92d5664a4f..c836c14233 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -43,16 +43,12 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { try { User user = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); if (user == null) { - throw new RuntimeException("[{" + tenantId + "}] userUpdateMsg {" + userUpdateMsg + "} cannot be converted to user"); + 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); - } + isCreated = userById == null; + user.setId(isCreated ? null : userId); String userEmail = user.getEmail(); User existing = edgeCtx.getUserService().findUserByTenantIdAndEmail(tenantId, user.getEmail()); @@ -85,7 +81,7 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { protected void updateUserCredentials(TenantId tenantId, UserCredentialsUpdateMsg updateMsg) { UserCredentials userCredentialsFromUpdateMsg = JacksonUtil.fromString(updateMsg.getEntity(), UserCredentials.class, true); if (userCredentialsFromUpdateMsg == null) { - throw new RuntimeException(String.format("[%s] Failed to parse UserCredentials from updateMsg: %s", tenantId, updateMsg)); + throw new IllegalArgumentException(String.format("[%s] Failed to parse UserCredentials from updateMsg: %s", tenantId, updateMsg)); } User user = edgeCtx.getUserService().findUserById(tenantId, userCredentialsFromUpdateMsg.getUserId()); @@ -111,7 +107,6 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { updated.setPassword(userCredentialsFromUpdateMsg.getPassword()); updated.setResetToken(userCredentialsFromUpdateMsg.getResetToken()); - if (created) { edgeCtx.getUserService().saveUserCredentials(tenantId, updated, false); } else { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index e0bd07c6c6..e7813caae9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -96,7 +96,7 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso pushUserCreatedEventToRuleEngine(tenantId, edge, userId); } - Boolean userEmailUpdated = resultPair.getSecond(); + boolean userEmailUpdated = resultPair.getSecond(); if (userEmailUpdated) { saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.USER, EdgeEventActionType.UPDATED, userId, null); diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index 84442e2d0a..3e8abd645c 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -53,188 +53,75 @@ public class UserEdgeTest extends AbstractEdgeTest { 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 public void testCreateUpdateDeleteTenantUser() throws Exception { // create user - edgeImitator.expectMessageAmount(3); - User newTenantAdmin = buildUser(Authority.TENANT_ADMIN, null, "tenantAdmin@thingsboard.org", DEFAULT_FIRST_NAME, "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()); - - UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg(); - User userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); - Assert.assertNotNull(userMsg); - Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); + User newTenantAdmin = buildUser(Authority.TENANT_ADMIN, null); + User savedTenantAdmin = createAndVerifyUserOnEdge(newTenantAdmin); // update user - edgeImitator.expectMessageAmount(2); - savedTenantAdmin.setLastName(UPDATED_LAST_NAME); - savedTenantAdmin = doPost("/api/user", savedTenantAdmin, User.class); - Assert.assertTrue(edgeImitator.waitForMessages()); - - userUpdateMsg = getLatestUserUpdateMsg(); - User userFromMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); - Assert.assertNotNull(userFromMsg); - Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(UPDATED_LAST_NAME, userFromMsg.getLastName()); + updateAndVerifyUserLastName(savedTenantAdmin); // update user credentials login(savedTenantAdmin.getEmail(), "tenant"); - - edgeImitator.expectMessageAmount(1); - ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); - changePasswordRequest.setCurrentPassword("tenant"); - changePasswordRequest.setNewPassword("newTenant"); - doPost("/api/auth/changePassword", changePasswordRequest); - 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(savedTenantAdmin.getId(), userCredentialsMsg.getUserId()); - Assert.assertTrue(passwordEncoder.matches(changePasswordRequest.getNewPassword(), userCredentialsMsg.getPassword())); - + updateAndVerifyUserCredentials(savedTenantAdmin); loginTenantAdmin(); // delete user - edgeImitator.expectMessageAmount(1); - doDelete("/api/user/" + savedTenantAdmin.getUuidId()) - .andExpect(status().isOk()); - Assert.assertTrue(edgeImitator.waitForMessages()); - - latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserUpdateMsg); - userUpdateMsg = (UserUpdateMsg) latestMessage; - Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedTenantAdmin.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); - Assert.assertEquals(savedTenantAdmin.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); + deleteAndVerifyUser(savedTenantAdmin); } @Test public void testCreateUpdateDeleteCustomerUser() throws Exception { // create customer - Customer savedCustomer = createAndAssignCustomerToEdge("Edge Customer"); + Customer savedCustomer = createAndAssignCustomerToEdge(); // create user - edgeImitator.expectMessageAmount(3); - User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId(), "customerUser@thingsboard.org", "John", "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(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); - Optional 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(savedCustomerUser.getId(), userMsg.getId()); - Assert.assertEquals(savedCustomerUser.getCustomerId(), userMsg.getCustomerId()); - Assert.assertEquals(savedCustomerUser.getAuthority(), userMsg.getAuthority()); - Assert.assertEquals(savedCustomerUser.getEmail(), userMsg.getEmail()); - Assert.assertEquals(savedCustomerUser.getFirstName(), userMsg.getFirstName()); - Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName()); + User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId()); + User savedCustomerUser = createAndVerifyUserOnEdge(customerUser); // update user - edgeImitator.expectMessageAmount(2); - savedCustomerUser.setLastName("Addams"); - savedCustomerUser = doPost("/api/user", savedCustomerUser, 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(savedCustomerUser.getLastName(), userMsg.getLastName()); + updateAndVerifyUserLastName(savedCustomerUser); // update user credentials login(savedCustomerUser.getEmail(), "customer"); - - edgeImitator.expectMessageAmount(1); - ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); - changePasswordRequest.setCurrentPassword("customer"); - changePasswordRequest.setNewPassword("newCustomer"); - doPost("/api/auth/changePassword", changePasswordRequest); - 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())); - + updateAndVerifyUserCredentials(savedCustomerUser); loginTenantAdmin(); // delete user - edgeImitator.expectMessageAmount(1); - doDelete("/api/user/" + savedCustomerUser.getUuidId()) - .andExpect(status().isOk()); - Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserUpdateMsg); - userUpdateMsg = (UserUpdateMsg) latestMessage; - Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomerUser.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); - Assert.assertEquals(savedCustomerUser.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); - + deleteAndVerifyUser(savedCustomerUser); } @Test public void testSendUserToCloudFromEdge() throws Exception { // create customer - Customer savedCustomer = createAndAssignCustomerToEdge("Edge Customer"); - - // create user - User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId(), "customerUser@thingsboard.org", DEFAULT_FIRST_NAME, "Johnson"); - - UUID uuid = UUID.randomUUID(); - customerUser.setId(new UserId(uuid)); - UUID userCredentialsUuid = UUID.randomUUID(); - UplinkMsg uplinkMsg = constructUserUplinkMsg(customerUser, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userCredentialsUuid); + Customer savedCustomer = createAndAssignCustomerToEdge(); - edgeImitator.expectResponsesAmount(1); - edgeImitator.sendUplinkMsg(uplinkMsg); - Assert.assertTrue(edgeImitator.waitForResponses()); - - User userFromCloud = doGet("/api/user/" + uuid, User.class); - Assert.assertNotNull(userFromCloud); - Assert.assertEquals(customerUser.getEmail(), userFromCloud.getEmail()); - //check user with existing email - User userWithExistingEmail = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId(), "customerUser@thingsboard.org", DEFAULT_FIRST_NAME, "Johnson"); - - UUID uuidForExistingEmail = UUID.randomUUID(); - userWithExistingEmail.setId(new UserId(uuidForExistingEmail)); - UUID userCredentialsUuidForExistingEmail = UUID.randomUUID(); - UplinkMsg uplinkMsgForExistingEmail = constructUserUplinkMsg(userWithExistingEmail, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userCredentialsUuidForExistingEmail); - - edgeImitator.expectResponsesAmount(1); - edgeImitator.sendUplinkMsg(uplinkMsgForExistingEmail); - Assert.assertTrue(edgeImitator.waitForResponses()); - - User userFromCloudWithExistingEmail = doGet("/api/user/" + uuidForExistingEmail, User.class); - Assert.assertNotNull(userFromCloudWithExistingEmail); - Assert.assertNotEquals(userWithExistingEmail.getEmail(), userFromCloudWithExistingEmail.getEmail()); + // 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); - UplinkMsg enabledCredentialsUplinkMsg = constructUserCredentialsUplinkMsg(customerUser.getId(), "password", true, userCredentialsUuid); + // create uplinkMsg with enabled userCredentials + UplinkMsg uplinkMsgWithEnabledCredentials = constructUserCredentialsUplinkMsg(userCredentialsId, userId); - edgeImitator.expectResponsesAmount(1); - edgeImitator.sendUplinkMsg(enabledCredentialsUplinkMsg); - Assert.assertTrue(edgeImitator.waitForResponses()); + User cloudUserWithCredentials = verifyMsgOnCloud(uplinkMsgWithEnabledCredentials, userId, false); + assertUserCredentialsFlags(cloudUserWithCredentials, true, true); - User cloudUserWithCredentials = doGet("/api/user/" + uuid, User.class); - Assert.assertNotNull(cloudUserWithCredentials); + // 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); - assertUserCredentialsFlags(cloudUserWithCredentials, true, true); + verifyMsgOnCloud(uplinkMsgForUserExistingEmail, secondUserId, true); } @Test @@ -264,40 +151,111 @@ public class UserEdgeTest extends AbstractEdgeTest { testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); } - private User buildUser(Authority authority, CustomerId customerId, String email, String firstName, String lastName) { + private Customer createAndAssignCustomerToEdge() throws Exception { + edgeImitator.expectMessageAmount(1); + Customer customer = new Customer(); + customer.setTitle("Edge Customer"); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + Assert.assertFalse(edgeImitator.waitForMessages(5)); + + edgeImitator.expectMessageAmount(2); + doPost("/api/customer/" + savedCustomer.getUuidId() + "/edge/" + edge.getUuidId(), Edge.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + return savedCustomer; + } + + private User buildUser(Authority authority, CustomerId customerId) { User customerUser = new User(); + customerUser.setAuthority(authority); customerUser.setTenantId(tenantId); customerUser.setCustomerId(customerId); - customerUser.setEmail(email); - customerUser.setFirstName(firstName); - customerUser.setLastName(lastName); + customerUser.setEmail(authority == Authority.TENANT_ADMIN ? DEFAULT_TENANT_ADMIN_EMAIL : DEFAULT_CUSTOMER_USER_EMAIL); + customerUser.setFirstName(DEFAULT_FIRST_NAME); + customerUser.setLastName(DEFAULT_LAST_NAME); + return customerUser; } - private Customer createAndAssignCustomerToEdge(String title) throws Exception { - edgeImitator.expectMessageAmount(1); - Customer customer = new Customer(); - customer.setTitle(title); - Customer savedCustomer = doPost("/api/customer", customer, Customer.class); - Assert.assertFalse(edgeImitator.waitForMessages(5)); + private User createAndVerifyUserOnEdge(User user) throws Exception { + // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user) + edgeImitator.expectMessageAmount(3); + User savedUser = createUser(user, user.getAuthority() == Authority.TENANT_ADMIN ? "tenant" : "customer"); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); + Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); + + UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg(); + User userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); + Assert.assertNotNull(userMsg); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); + Assert.assertEquals(savedUser.getId(), userMsg.getId()); + Assert.assertEquals(savedUser.getCustomerId(), userMsg.getCustomerId()); + Assert.assertEquals(savedUser.getAuthority(), userMsg.getAuthority()); + Assert.assertEquals(savedUser.getEmail(), userMsg.getEmail()); + Assert.assertEquals(savedUser.getFirstName(), userMsg.getFirstName()); + Assert.assertEquals(savedUser.getLastName(), userMsg.getLastName()); + + return savedUser; + } + + private void updateAndVerifyUserLastName(User user) throws Exception { + user.setLastName(UPDATED_LAST_NAME); edgeImitator.expectMessageAmount(2); - doPost("/api/customer/" + savedCustomer.getUuidId() + "/edge/" + edge.getUuidId(), Edge.class); + doPost("/api/user", user, User.class); Assert.assertTrue(edgeImitator.waitForMessages()); - return savedCustomer; + UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg(); + User userFromMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); + Assert.assertNotNull(userFromMsg); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); + Assert.assertEquals(UPDATED_LAST_NAME, userFromMsg.getLastName()); } - private UplinkMsg constructUserUplinkMsg(User user, UpdateMsgType msgType, UUID userCredentialsUuid) { - UserUpdateMsg userUpdateMsg = EdgeMsgConstructorUtils.constructUserUpdatedMsg(msgType, user); + private void updateAndVerifyUserCredentials(User user) throws Exception { + String password = user.getAuthority() == Authority.TENANT_ADMIN ? "tenant" : "customer"; + String newPassword = "new" + password; - UserCredentials userCredentials = new UserCredentials(); - userCredentials.setId(new UserCredentialsId(userCredentialsUuid)); - userCredentials.setUserId(user.getId()); - userCredentials.setEnabled(false); - userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode()); + edgeImitator.expectMessageAmount(1); + ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); + changePasswordRequest.setCurrentPassword(password); + changePasswordRequest.setNewPassword(newPassword); + doPost("/api/auth/changePassword", changePasswordRequest); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); + UserCredentialsUpdateMsg msg = (UserCredentialsUpdateMsg) latestMessage; + 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())); + } + + private void deleteAndVerifyUser(User savedTenantAdmin) throws Exception { + edgeImitator.expectMessageAmount(1); + doDelete("/api/user/" + savedTenantAdmin.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof UserUpdateMsg); + UserUpdateMsg userUpdateMsg = (UserUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType()); + Assert.assertEquals(savedTenantAdmin.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); + 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() @@ -306,13 +264,38 @@ public class UserEdgeTest extends AbstractEdgeTest { .build(); } - private UplinkMsg constructUserCredentialsUplinkMsg(UserId userId, String password, boolean enabled, UUID userCredentialsUuid) { + private UserCredentials buildCredentials(UserCredentialsId userCredentialsUuid, UserId userId, boolean enabled) { UserCredentials userCredentials = new UserCredentials(); - userCredentials.setId(new UserCredentialsId(userCredentialsUuid)); + + userCredentials.setId(userCredentialsUuid); userCredentials.setUserId(userId); userCredentials.setEnabled(enabled); - userCredentials.setPassword(password); + userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode()); + + return userCredentials; + } + + private User verifyMsgOnCloud(UplinkMsg uplinkMsg, UserId userId, boolean emailExist) throws Exception { + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsg); + Assert.assertTrue(edgeImitator.waitForResponses()); + + User userFromCloud = doGet("/api/user/" + userId, User.class); + Assert.assertNotNull(userFromCloud); + + if (emailExist) { + Assert.assertNotEquals(DEFAULT_CUSTOMER_USER_EMAIL, userFromCloud.getEmail()); + } else { + Assert.assertEquals(DEFAULT_CUSTOMER_USER_EMAIL, userFromCloud.getEmail()); + } + return userFromCloud; + } + + private UplinkMsg constructUserCredentialsUplinkMsg(UserCredentialsId userCredentialsUuid, UserId userId) { + UserCredentials userCredentials = buildCredentials(userCredentialsUuid, userId, true); + userCredentials.setPassword("password"); UserCredentialsUpdateMsg credsMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentials); + return UplinkMsg.newBuilder() .addUserCredentialsUpdateMsg(credsMsg) .build(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java index 64d0fc21b8..777ce4dfc8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java @@ -136,5 +136,4 @@ public class UserDataValidator extends DataValidator { } } } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 8c229c815d..7cbf1c80bd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -166,7 +166,6 @@ public class UserServiceImpl extends AbstractCachedEntityService Date: Thu, 9 Oct 2025 13:10:58 +0300 Subject: [PATCH 03/46] Refactor Test --- .../thingsboard/server/edge/UserEdgeTest.java | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index 3e8abd645c..8b5261150b 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.edge; import com.fasterxml.jackson.databind.JsonNode; -import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -125,7 +124,7 @@ public class UserEdgeTest extends AbstractEdgeTest { } @Test - public void testSendUserCredentialsRequest() throws Exception { + public void testSendUserCredentialsRequestToCloud() throws Exception { UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder(); userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits()); @@ -141,9 +140,7 @@ public class UserEdgeTest extends AbstractEdgeTest { Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); - UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; + UserCredentialsUpdateMsg userCredentialsUpdateMsg = getLatestUserCredentialsUpdateMsg(); UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); Assert.assertNotNull(userCredentialsMsg); Assert.assertEquals(tenantAdminUserId, userCredentialsMsg.getUserId()); @@ -166,24 +163,25 @@ public class UserEdgeTest extends AbstractEdgeTest { } private User buildUser(Authority authority, CustomerId customerId) { - User customerUser = new User(); + User user = new User(); - customerUser.setAuthority(authority); - customerUser.setTenantId(tenantId); - customerUser.setCustomerId(customerId); - customerUser.setEmail(authority == Authority.TENANT_ADMIN ? DEFAULT_TENANT_ADMIN_EMAIL : DEFAULT_CUSTOMER_USER_EMAIL); - customerUser.setFirstName(DEFAULT_FIRST_NAME); - customerUser.setLastName(DEFAULT_LAST_NAME); + 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 customerUser; + 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); - User savedUser = createUser(user, user.getAuthority() == Authority.TENANT_ADMIN ? "tenant" : "customer"); + User savedUser = createUser(user, password); Assert.assertTrue(edgeImitator.waitForMessages()); - Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); @@ -226,9 +224,7 @@ public class UserEdgeTest extends AbstractEdgeTest { doPost("/api/auth/changePassword", changePasswordRequest); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); - UserCredentialsUpdateMsg msg = (UserCredentialsUpdateMsg) latestMessage; + UserCredentialsUpdateMsg msg = getLatestUserCredentialsUpdateMsg(); UserCredentials creds = JacksonUtil.fromString(msg.getEntity(), UserCredentials.class, true); Assert.assertNotNull(creds); Assert.assertEquals(user.getId(), creds.getUserId()); @@ -241,9 +237,7 @@ public class UserEdgeTest extends AbstractEdgeTest { .andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserUpdateMsg); - UserUpdateMsg userUpdateMsg = (UserUpdateMsg) latestMessage; + UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg(); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType()); Assert.assertEquals(savedTenantAdmin.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); Assert.assertEquals(savedTenantAdmin.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); @@ -314,4 +308,10 @@ public class UserEdgeTest extends AbstractEdgeTest { return opt.get(); } + private UserCredentialsUpdateMsg getLatestUserCredentialsUpdateMsg() { + Optional opt = edgeImitator.findMessageByType(UserCredentialsUpdateMsg.class); + Assert.assertTrue(opt.isPresent()); + return opt.get(); + } + } From 267997fb1ab540780e10abd146b4c557d92b6f2b Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 18 Nov 2025 18:14:33 +0200 Subject: [PATCH 04/46] noxss for AI model and oauth2 client --- .../java/org/thingsboard/server/common/data/ai/AiModel.java | 2 ++ .../org/thingsboard/server/common/data/oauth2/OAuth2Client.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java index 4d7bb21930..3564f8dee0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java +++ b/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.validation.Length; import org.thingsboard.server.common.data.validation.NoNullChar; +import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serial; @@ -64,6 +65,7 @@ public final class AiModel extends BaseData implements HasTenantId, H @NotBlank @NoNullChar @Length(min = 1, max = 255) + @NoXss @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java index 6396ecb7b8..e6b6c1ff6f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java +++ b/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.TenantId; import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; import java.util.List; @@ -42,6 +43,7 @@ public class OAuth2Client extends BaseDataWithAdditionalInfo imp private TenantId tenantId; @Schema(description = "Oauth2 client title") @NotBlank + @NoXss @Length(fieldName = "title", max = 100, message = "cannot be longer than 100 chars") private String title; @Schema(description = "Config for mapping OAuth2 log in response to platform entities", requiredMode = Schema.RequiredMode.REQUIRED) From 9085b42d4304c000f5a66cfab275899458300ce1 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Fri, 21 Nov 2025 14:26:47 +0200 Subject: [PATCH 05/46] Add handling of ENTITY_DELETED_RPC_MESSAGE during edge user message processing --- .../rpc/processor/user/BaseUserProcessor.java | 10 +++++++- .../rpc/processor/user/UserEdgeProcessor.java | 14 ++++++++++- .../thingsboard/server/edge/UserEdgeTest.java | 25 +++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java index c836c14233..14e4da53c0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -78,6 +78,15 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { return Pair.of(isCreated, userEmailUpdated); } + protected User deleteUser(TenantId tenantId, UserId userId) { + User userById = edgeCtx.getUserService().findUserById(tenantId, userId); + if (userById == null) { + throw new IllegalArgumentException(String.format("[%s] Failed to find User with id [%s]", tenantId, userId)); + } + edgeCtx.getUserService().deleteUser(tenantId, userById); + return userById; + } + protected void updateUserCredentials(TenantId tenantId, UserCredentialsUpdateMsg updateMsg) { UserCredentials userCredentialsFromUpdateMsg = JacksonUtil.fromString(updateMsg.getEntity(), UserCredentials.class, true); if (userCredentialsFromUpdateMsg == null) { @@ -117,7 +126,6 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { tenantId, user.getName(), updateMsg, e); throw new RuntimeException(e); } - } protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, User user, UserUpdateMsg userUpdateMsg); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index e7813caae9..cfe888b1a5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -61,6 +61,10 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso 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) { @@ -116,7 +120,15 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso } } - @Override + private void deleteUserAndPushEntityDeletedEventToRuleEngine(TenantId tenantId, UserId userId, Edge edge) { + User removedUser = deleteUser(tenantId, userId); + CustomerId userCustomerId = removedUser.getCustomerId(); + String userAsString = JacksonUtil.toString(removedUser); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, userCustomerId); + pushEntityEventToRuleEngine(tenantId, userId, userCustomerId, TbMsgType.ENTITY_DELETED, userAsString, msgMetaData); + } + + @Override public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { UserId userId = new UserId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index 8b5261150b..f590b21b84 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -148,6 +148,31 @@ public class UserEdgeTest extends AbstractEdgeTest { testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); } + @Test + public void testSendUserDeleteFromEdgeToCloud() throws Exception { + // 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 + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + UserUpdateMsg.newBuilder().setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(savedCustomerUser.getUuidId().getMostSignificantBits()) + .setIdLSB(savedCustomerUser.getUuidId().getLeastSignificantBits()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + // expect edge message sent & cloud message response + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + } + private Customer createAndAssignCustomerToEdge() throws Exception { edgeImitator.expectMessageAmount(1); Customer customer = new Customer(); From 12ed2a56be5e728a6e8e3f127db40c534fa595c5 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Fri, 21 Nov 2025 15:40:37 +0200 Subject: [PATCH 06/46] Remove exception when user doesn't exist during edge user delete event processing --- .../service/edge/rpc/processor/user/BaseUserProcessor.java | 3 ++- .../service/edge/rpc/processor/user/UserEdgeProcessor.java | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java index 14e4da53c0..897e49a7cd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -81,7 +81,8 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { protected User deleteUser(TenantId tenantId, UserId userId) { User userById = edgeCtx.getUserService().findUserById(tenantId, userId); if (userById == null) { - throw new IllegalArgumentException(String.format("[%s] Failed to find User with id [%s]", tenantId, userId)); + log.trace("[{}] User with id {} does not exist", tenantId, userId); + return null; } edgeCtx.getUserService().deleteUser(tenantId, userById); return userById; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index cfe888b1a5..7905e802bd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -122,6 +122,9 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso private 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 = getEdgeActionTbMsgMetaData(edge, userCustomerId); From bc1af0ba3bc290ecd7bf5f95a01782a977be5171 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Fri, 21 Nov 2025 17:04:42 +0200 Subject: [PATCH 07/46] Fix testSendUserDeleteFromEdgeToCloud --- .../rpc/processor/user/UserEdgeProcessor.java | 2 +- .../thingsboard/server/edge/UserEdgeTest.java | 28 +++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index 7905e802bd..968d573618 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -131,7 +131,7 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso pushEntityEventToRuleEngine(tenantId, userId, userCustomerId, TbMsgType.ENTITY_DELETED, userAsString, msgMetaData); } - @Override + @Override public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { UserId userId = new UserId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index f590b21b84..6bf51eb159 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -20,8 +20,11 @@ import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; 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.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.edge.Edge; @@ -41,6 +44,7 @@ import org.thingsboard.server.service.security.model.ChangePasswordRequest; 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.thingsboard.server.dao.user.UserServiceImpl.DEFAULT_TOKEN_LENGTH; @@ -158,19 +162,31 @@ public class UserEdgeTest extends AbstractEdgeTest { User savedCustomerUser = createAndVerifyUserOnEdge(customerUser); // simulate user removal event from edge to cloud - UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); - UserUpdateMsg.newBuilder().setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + UserUpdateMsg.Builder userUpdateMsg = UserUpdateMsg.newBuilder().setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) .setIdMSB(savedCustomerUser.getUuidId().getMostSignificantBits()) .setIdLSB(savedCustomerUser.getUuidId().getLeastSignificantBits()); - testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + UplinkMsg uplink = UplinkMsg.newBuilder() + .setUplinkMsgId(EdgeUtils.nextPositiveInt()) + .addUserUpdateMsg(userUpdateMsg).build(); + + testAutoGeneratedCodeByProtobuf(userUpdateMsg); // expect edge message sent & cloud message response edgeImitator.expectResponsesAmount(1); - edgeImitator.expectMessageAmount(1); - edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + edgeImitator.sendUplinkMsg(uplink); Assert.assertTrue(edgeImitator.waitForResponses()); - Assert.assertTrue(edgeImitator.waitForMessages()); + + 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 { From 1315733d1e2db20c1f904dbd5db1a120eddefd45 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Mon, 24 Nov 2025 12:11:41 +0200 Subject: [PATCH 08/46] Revert userCredentials.setId(null); in UserServiceImpl#replaceUserCredentialsInternal --- .../java/org/thingsboard/server/dao/user/UserServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index f2b9e476b5..bdcac80d4a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -361,7 +361,7 @@ public class UserServiceImpl extends AbstractCachedEntityService Date: Mon, 24 Nov 2025 14:23:26 +0200 Subject: [PATCH 09/46] Fix overriding exiting user credential ID before calling replaceUserCredentials on BaseUserProcessor --- .../service/edge/rpc/processor/user/BaseUserProcessor.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java index 897e49a7cd..0da0e4aade 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.service.DataValidator; @@ -107,6 +108,7 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { try { UserCredentials existing = edgeCtx.getUserService().findUserCredentialsByUserId(tenantId, user.getId()); boolean created = existing == null; + UserCredentialsId oldCredentialsId = created ? null : existing.getId(); UserCredentials updated = created ? new UserCredentials() : existing; updated.setId(userCredentialsFromUpdateMsg.getId()); @@ -120,7 +122,7 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { if (created) { edgeCtx.getUserService().saveUserCredentials(tenantId, updated, false); } else { - edgeCtx.getUserService().replaceUserCredentials(tenantId, updated, existing.getId(), false); + edgeCtx.getUserService().replaceUserCredentials(tenantId, updated, oldCredentialsId, false); } } catch (Exception e) { log.error("[{}] Can't update user credentials for user [{}], userCredentialsUpdateMsg [{}]", From a62a41e299967e7725279ea9a20be90925f6d56e Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 17 Nov 2025 10:32:16 +0200 Subject: [PATCH 10/46] fixed validation of change password fields --- .../pages/security/security.component.html | 10 +- .../home/pages/security/security.component.ts | 82 +++++------- .../app/modules/login/login-routing.module.ts | 34 ++++- ui-ngx/src/app/modules/login/login.module.ts | 4 +- .../login/create-password.component.html | 22 +++- .../pages/login/create-password.component.ts | 62 +++++---- ...ssword-requirements-tooltip.component.html | 35 +++++ ...ssword-requirements-tooltip.component.scss | 48 +++++++ ...password-requirements-tooltip.component.ts | 57 ++++++++ .../pages/login/reset-password.component.html | 20 ++- .../pages/login/reset-password.component.ts | 62 +++++---- .../src/app/shared/models/password.models.ts | 122 ++++++++++++++++++ ui-ngx/src/app/shared/models/public-api.ts | 1 + .../assets/locale/locale.constant-en_US.json | 12 ++ 14 files changed, 458 insertions(+), 113 deletions(-) create mode 100644 ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.html create mode 100644 ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.scss create mode 100644 ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.ts create mode 100644 ui-ngx/src/app/shared/models/password.models.ts diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.html b/ui-ngx/src/app/modules/home/pages/security/security.component.html index 5b661963c6..4d7af5bfd0 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.component.html +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.html @@ -40,7 +40,7 @@ profile.current-password - + {{ 'security.password-requirement.incorrect-password-try-again' | translate }} @@ -52,13 +52,13 @@ + && !changePassword.get('newPassword').hasError('passwordSameAsOld')"> {{ 'security.password-requirement.password-not-meet-requirements' | translate }} {{ changePassword.get('newPassword').getError('alreadyUsed') }} - + {{ 'security.password-requirement.password-should-difference' | translate }} @@ -72,7 +72,7 @@ login.new-password-again - + {{ 'security.password-requirement.new-passwords-not-match' | translate }} @@ -134,7 +134,7 @@ diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.ts b/ui-ngx/src/app/modules/home/pages/security/security.component.ts index 32094d9677..4bad58a764 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.ts @@ -23,7 +23,6 @@ import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, FormGroupDirective, - NgForm, ValidationErrors, ValidatorFn, Validators @@ -48,10 +47,14 @@ import { import { authenticationDialogMap } from '@home/pages/security/authentication-dialog/authentication-dialog.map'; import { takeUntil, tap } from 'rxjs/operators'; 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 { UserPasswordPolicy } from '@shared/models/settings.models'; import { MatCheckboxChange } from '@angular/material/checkbox'; +import { + passwordsMatchValidator, + passwordStrengthValidator +} from '@shared/models/password.models'; @Component({ selector: 'tb-security', @@ -164,7 +167,12 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro this.changePassword = this.fb.group({ currentPassword: [''], newPassword: ['', Validators.required], - newPassword2: ['', this.samePasswordValidation(false, 'newPassword')] + newPassword2: [''] + }, { + validators: [ + this.passwordNotSameAsOld(), + passwordsMatchValidator('newPassword', 'newPassword2'), + ] }); } @@ -172,64 +180,36 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro this.authService.getUserPasswordPolicy().subscribe(policy => { this.passwordPolicy = policy; this.changePassword.get('newPassword').setValidators([ - this.passwordStrengthValidator(), - this.samePasswordValidation(true, 'currentPassword'), + passwordStrengthValidator(this.passwordPolicy), Validators.required ]); this.changePassword.get('newPassword').updateValueAndValidity({emitEvent: false}); }); } - private passwordStrengthValidator(): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - const value: string = control.value; - const errors: any = {}; - - 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 && - !new RegExp(`(?:.*?[\\W_]){${this.passwordPolicy.minimumSpecialCharacters}}`).test(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) { - errors.maxLength = true; - } + passwordNotSameAsOld(): ValidatorFn { + return (group: AbstractControl): ValidationErrors | null => { + const currentPassControl = group.get('currentPassword'); + const newPassControl = group.get('newPassword'); - return isEqual(errors, {}) ? null : errors; - }; - } - private samePasswordValidation(isSame: boolean, key: string): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - const value: string = control.value; - const keyValue = control.parent?.value[key]; + const current = currentPassControl?.value ?? ''; + const newPass = newPassControl?.value ?? ''; - if (isSame) { - return value === keyValue ? {samePassword: true} : null; + if (current && newPass && current === newPass) { + newPassControl?.setErrors({ + ...newPassControl.errors, + passwordSameAsOld: true + }); + return { passwordSameAsOld: true }; + } else { + const currentErrors = newPassControl?.errors; + if (currentErrors?.passwordSameAsOld) { + const { passwordSameAsOld, ...rest } = currentErrors; + newPassControl.setErrors(Object.keys(rest).length ? rest : null); + } + return null; } - return value !== keyValue ? {differencePassword: true} : null; }; } diff --git a/ui-ngx/src/app/modules/login/login-routing.module.ts b/ui-ngx/src/app/modules/login/login-routing.module.ts index 3000d81fb8..da4ac05def 100644 --- a/ui-ngx/src/app/modules/login/login-routing.module.ts +++ b/ui-ngx/src/app/modules/login/login-routing.module.ts @@ -14,8 +14,8 @@ /// limitations under the License. /// -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { inject, NgModule } from '@angular/core'; +import { ActivatedRouteSnapshot, ResolveFn, Router, RouterModule, RouterStateSnapshot, Routes } from '@angular/router'; import { LoginComponent } from './pages/login/login.component'; import { AuthGuard } from '@core/guards/auth.guard'; @@ -25,6 +25,21 @@ import { CreatePasswordComponent } from '@modules/login/pages/login/create-passw import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component'; import { Authority } from '@shared/models/authority.enum'; import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.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 = (route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + router = inject(Router), + authService = inject(AuthService)) => { + return authService.getUserPasswordPolicy().pipe( + catchError(() => { + return of({} as UserPasswordPolicy); + }) + ); +}; const routes: Routes = [ { @@ -52,7 +67,10 @@ const routes: Routes = [ title: 'login.reset-password', module: 'public' }, - canActivate: [AuthGuard] + canActivate: [AuthGuard], + resolve: { + passwordPolicy: passwordPolicyResolver + } }, { path: 'login/resetExpiredPassword', @@ -62,7 +80,10 @@ const routes: Routes = [ module: 'public', expiredPassword: true }, - canActivate: [AuthGuard] + canActivate: [AuthGuard], + resolve: { + passwordPolicy: passwordPolicyResolver + } }, { path: 'login/createPassword', @@ -71,7 +92,10 @@ const routes: Routes = [ title: 'login.create-password', module: 'public' }, - canActivate: [AuthGuard] + canActivate: [AuthGuard], + resolve: { + passwordPolicy: passwordPolicyResolver + } }, { path: 'login/mfa', diff --git a/ui-ngx/src/app/modules/login/login.module.ts b/ui-ngx/src/app/modules/login/login.module.ts index 35dbfad7e2..a7d8731f9d 100644 --- a/ui-ngx/src/app/modules/login/login.module.ts +++ b/ui-ngx/src/app/modules/login/login.module.ts @@ -25,6 +25,7 @@ import { ResetPasswordComponent } from '@modules/login/pages/login/reset-passwor import { CreatePasswordComponent } from '@modules/login/pages/login/create-password.component'; import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component'; import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component'; +import { PasswordRequirementsTooltipComponent } from '@modules/login/pages/login/password-requirements-tooltip.component'; @NgModule({ declarations: [ @@ -33,7 +34,8 @@ import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.co ResetPasswordComponent, CreatePasswordComponent, TwoFactorAuthLoginComponent, - LinkExpiredComponent + LinkExpiredComponent, + PasswordRequirementsTooltipComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html index e5b287df16..79b601d099 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html @@ -32,15 +32,28 @@ common.password - + lock + + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} + login.password-again - + lock + + {{ 'security.password-requirement.new-passwords-not-match' | translate }} +
+ + diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts index 4cb8fa7cba..e7aa68cba0 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts @@ -14,57 +14,71 @@ /// limitations under the License. /// -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; 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 { UntypedFormBuilder } from '@angular/forms'; -import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { ActivatedRoute } from '@angular/router'; -import { Subscription } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { UserPasswordPolicy } from '@shared/models/settings.models'; +import { combineLatest } from 'rxjs'; +import { + passwordsMatchValidator, + passwordStrengthValidator +} from '@shared/models/password.models'; @Component({ selector: 'tb-create-password', templateUrl: './create-password.component.html', styleUrls: ['./create-password.component.scss'] }) -export class CreatePasswordComponent extends PageComponent implements OnInit, OnDestroy { +export class CreatePasswordComponent extends PageComponent { activateToken = ''; - sub: Subscription; - - createPassword = this.fb.group({ - password: [''], - password2: [''] - }); + createPassword: UntypedFormGroup; + passwordPolicy: UserPasswordPolicy; constructor(protected store: Store, private route: ActivatedRoute, private authService: AuthService, private translate: TranslateService, - public fb: UntypedFormBuilder) { + private fb: UntypedFormBuilder) { super(store); - } - ngOnInit() { - this.sub = this.route - .queryParams - .subscribe(params => { - this.activateToken = params.activateToken || ''; + combineLatest([ + this.route.queryParams, + this.route.data + ]) + .pipe(takeUntilDestroyed()) + .subscribe(([params, data]) => { + this.activateToken = params['activateToken'] || ''; + this.passwordPolicy = data['passwordPolicy']; }); + + this.buildCreatePasswordForm(); + } + + private buildCreatePasswordForm() { + this.createPassword = this.fb.group({ + newPassword: ['', [Validators.required, passwordStrengthValidator(this.passwordPolicy)]], + newPassword2:[''] + }, { + validators: [ + passwordsMatchValidator('newPassword', 'newPassword2'), + ] + }); } - ngOnDestroy(): void { - super.ngOnDestroy(); - this.sub.unsubscribe(); + get passwordErrorsLength(): number { + return Object.keys(this.createPassword.get('newPassword')?.errors ?? {}).length; } onCreatePassword() { - if (this.createPassword.get('password').value !== this.createPassword.get('password2').value) { - this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'), - type: 'error' })); + if (this.createPassword.invalid) { + this.createPassword.markAllAsTouched(); } else { this.authService.activate( this.activateToken, diff --git a/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.html b/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.html new file mode 100644 index 0000000000..f54b8c64ef --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.html @@ -0,0 +1,35 @@ + + +
+ @for (rule of passwordErrorRules; track $index) { + @if (!rule.policyProp || passwordPolicy[rule.policyProp] > 0) { +

+ + {{ checkForError(rule.key) ? 'mdi:close' : 'mdi:check' }} + + {{ rule.translation | translate : passwordPolicy }} +

+ } + } +
+
+
diff --git a/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.scss b/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.scss new file mode 100644 index 0000000000..10ed07030d --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/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; + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.ts b/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.ts new file mode 100644 index 0000000000..9d8c4d9b66 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.ts @@ -0,0 +1,57 @@ +/// +/// 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 } 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'; + +@Component({ + selector: 'tb-password-requirements-tooltip', + templateUrl: './password-requirements-tooltip.component.html', + styleUrl: './password-requirements-tooltip.component.scss' +}) +export class PasswordRequirementsTooltipComponent { + @Input() passwordControl: AbstractControl; + @Input() passwordPolicy: UserPasswordPolicy; + @Input() trigger: CdkOverlayOrigin; + + passwordErrorRules = passwordErrorRules; + isTooltipOpen = false; + + overlayPositions: ConnectionPositionPair[] = [ + { + originX: 'center', + originY: 'top', + overlayX: 'center', + overlayY: 'bottom', + offsetY: -20 + } + ]; + + checkForError(errorName: string): boolean { + return this.passwordControl?.hasError(errorName) ?? false; + } + + onFocus(): void { + this.isTooltipOpen = true; + } + + onBlur(): void { + this.isTooltipOpen = false; + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html index 5233189902..b4be2ecc9d 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html @@ -35,15 +35,28 @@ login.new-password - + lock + + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} + login.new-password-again lock + + {{ 'security.password-requirement.new-passwords-not-match' | translate }} +
+ + diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts index 202722cf15..4a2c0bc75d 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts @@ -14,61 +14,75 @@ /// limitations under the License. /// -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; 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 { UntypedFormBuilder } from '@angular/forms'; -import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { Subscription } from 'rxjs'; +import { combineLatest } from 'rxjs'; +import { UserPasswordPolicy } from '@shared/models/settings.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + passwordsMatchValidator, + passwordStrengthValidator +} from '@shared/models/password.models'; @Component({ selector: 'tb-reset-password', templateUrl: './reset-password.component.html', styleUrls: ['./reset-password.component.scss'] }) -export class ResetPasswordComponent extends PageComponent implements OnInit, OnDestroy { +export class ResetPasswordComponent extends PageComponent { isExpiredPassword: boolean; resetToken = ''; - sub: Subscription; - resetPassword = this.fb.group({ - newPassword: [''], - newPassword2: [''] - }); + resetPassword: UntypedFormGroup; + passwordPolicy: UserPasswordPolicy; constructor(protected store: Store, private route: ActivatedRoute, private router: Router, private authService: AuthService, private translate: TranslateService, - public fb: UntypedFormBuilder) { + private fb: UntypedFormBuilder) { super(store); + combineLatest([ + this.route.queryParams, + this.route.data + ]) + .pipe(takeUntilDestroyed()) + .subscribe(([params, data]) => { + this.resetToken = params['resetToken'] || ''; + this.passwordPolicy = data['passwordPolicy']; + this.isExpiredPassword = data['expiredPassword'] ?? false; + }); + + this.buildResetPasswordForm(); } - ngOnInit() { - this.isExpiredPassword = this.route.snapshot.data.expiredPassword; - this.sub = this.route - .queryParams - .subscribe(params => { - this.resetToken = params.resetToken || ''; - }); + private buildResetPasswordForm() { + this.resetPassword = this.fb.group({ + newPassword: ['', [Validators.required, passwordStrengthValidator(this.passwordPolicy)]], + newPassword2: [''] + }, { + validators: [ + passwordsMatchValidator('newPassword', 'newPassword2'), + ] + }); } - ngOnDestroy(): void { - super.ngOnDestroy(); - this.sub.unsubscribe(); + get passwordErrorsLength(): number { + return Object.keys(this.resetPassword.get('newPassword')?.errors ?? {}).length; } onResetPassword() { - if (this.resetPassword.get('newPassword').value !== this.resetPassword.get('newPassword2').value) { - this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'), - type: 'error' })); + if (this.resetPassword.invalid) { + this.resetPassword.markAllAsTouched(); } else { this.authService.resetPassword( this.resetToken, diff --git a/ui-ngx/src/app/shared/models/password.models.ts b/ui-ngx/src/app/shared/models/password.models.ts new file mode 100644 index 0000000000..4bba3d6737 --- /dev/null +++ b/ui-ngx/src/app/shared/models/password.models.ts @@ -0,0 +1,122 @@ +/// +/// 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 PasswordErrorMessageKey { + minLength = 'security.password-requirement.password-min-length', + maxLength = 'security.password-requirement.password-max-length', + notUpperCase = 'security.password-requirement.password-uppercase', + notLowerCase = 'security.password-requirement.password-lowercase', + notNumeric = 'security.password-requirement.password-digit', + notSpecial = 'security.password-requirement.password-special-characters', + hasWhitespaces = 'security.password-requirement.password-should-not-contain-spaces', + default = 'security.password-requirement.password-not-meet-requirements' +} + +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' +} + +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 }, +]; + +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 ?? ''; + + const userInteracted = + confirmControl.touched || confirmControl.dirty || group.touched; + + if (!userInteracted) { + return null; + } + + if (newPass && 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; + }; +} diff --git a/ui-ngx/src/app/shared/models/public-api.ts b/ui-ngx/src/app/shared/models/public-api.ts index bd7fe18262..5f930ff180 100644 --- a/ui-ngx/src/app/shared/models/public-api.ts +++ b/ui-ngx/src/app/shared/models/public-api.ts @@ -71,3 +71,4 @@ export * from './query/query.models'; export * from './regex.constants'; export * from './trendz-settings.models'; export * from './ai-model.models'; +export * from './password.models'; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index ec3ef79ef4..8f3ef7e3d8 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4412,6 +4412,18 @@ "at-least": "At least:", "character": "{ count, plural, =1 {1 character} other {# characters} }", "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", + "password-min-length": "Password must be {{minimumLength}} or more characters in length", + "password-max-length": "Password should be less than {{maximumLength}}", + "password-uppercase": "Password must contain {{minimumUppercaseLetters}} or more uppercase characters", + "password-lowercase": "Password must contain {{minimumLowercaseLetters}} or more lowercase characters", + "password-digit": "Password must contain {{minimumDigits}} or more digit characters", + "password-special-characters": "Password must contain {{minimumSpecialCharacters}} or more special characters", "incorrect-password-try-again": "Incorrect password. Try again", "lowercase-letter": "{ count, plural, =1 {1 lowercase letter} other {# lowercase letters} }", "new-passwords-not-match": "New password didn't match", From 8a3d9048b2138d630a3ff622a00d92d72dfc2f1d Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 25 Nov 2025 11:39:12 +0200 Subject: [PATCH 11/46] moved password requirements tooltip to shared module --- ui-ngx/src/app/modules/login/login.module.ts | 4 +--- .../components}/password-requirements-tooltip.component.html | 0 .../components}/password-requirements-tooltip.component.scss | 0 .../components}/password-requirements-tooltip.component.ts | 0 ui-ngx/src/app/shared/shared.module.ts | 3 +++ 5 files changed, 4 insertions(+), 3 deletions(-) rename ui-ngx/src/app/{modules/login/pages/login => shared/components}/password-requirements-tooltip.component.html (100%) rename ui-ngx/src/app/{modules/login/pages/login => shared/components}/password-requirements-tooltip.component.scss (100%) rename ui-ngx/src/app/{modules/login/pages/login => shared/components}/password-requirements-tooltip.component.ts (100%) diff --git a/ui-ngx/src/app/modules/login/login.module.ts b/ui-ngx/src/app/modules/login/login.module.ts index a7d8731f9d..35dbfad7e2 100644 --- a/ui-ngx/src/app/modules/login/login.module.ts +++ b/ui-ngx/src/app/modules/login/login.module.ts @@ -25,7 +25,6 @@ import { ResetPasswordComponent } from '@modules/login/pages/login/reset-passwor import { CreatePasswordComponent } from '@modules/login/pages/login/create-password.component'; import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component'; import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component'; -import { PasswordRequirementsTooltipComponent } from '@modules/login/pages/login/password-requirements-tooltip.component'; @NgModule({ declarations: [ @@ -34,8 +33,7 @@ import { PasswordRequirementsTooltipComponent } from '@modules/login/pages/login ResetPasswordComponent, CreatePasswordComponent, TwoFactorAuthLoginComponent, - LinkExpiredComponent, - PasswordRequirementsTooltipComponent + LinkExpiredComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.html b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.html similarity index 100% rename from ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.html rename to ui-ngx/src/app/shared/components/password-requirements-tooltip.component.html diff --git a/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.scss b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.scss similarity index 100% rename from ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.scss rename to ui-ngx/src/app/shared/components/password-requirements-tooltip.component.scss diff --git a/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.ts b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts similarity index 100% rename from ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.ts rename to ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 9fd6e4a71e..c69776ef1d 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -228,6 +228,7 @@ import { JsFuncModuleRowComponent } from '@shared/components/js-func-module-row. import { EntityKeyAutocompleteComponent } from '@shared/components/entity/entity-key-autocomplete.component'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { MqttVersionSelectComponent } from '@shared/components/mqtt-version-select.component'; +import { PasswordRequirementsTooltipComponent } from '@shared/components/password-requirements-tooltip.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -443,6 +444,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ScadaSymbolInputComponent, EntityKeyAutocompleteComponent, MqttVersionSelectComponent, + PasswordRequirementsTooltipComponent ], imports: [ CommonModule, @@ -707,6 +709,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ScadaSymbolInputComponent, EntityKeyAutocompleteComponent, MqttVersionSelectComponent, + PasswordRequirementsTooltipComponent ] }) export class SharedModule { } From 792f5471201acf83103aa02f5bb06cf7670139b9 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 25 Nov 2025 15:55:38 +0200 Subject: [PATCH 12/46] debug events improvements --- .../server/actors/ActorSystemContext.java | 14 +-- ...CalculatedFieldEntityMessageProcessor.java | 14 +-- .../CalculatedFieldException.java | 8 +- .../ctx/state/BaseCalculatedFieldState.java | 8 ++ .../cf/ctx/state/CalculatedFieldCtx.java | 4 +- .../cf/ctx/state/CalculatedFieldState.java | 3 + ...EntityAggregationCalculatedFieldState.java | 89 +++++++++++++++++++ .../server/common/data/DataConstants.java | 2 + 8 files changed, 120 insertions(+), 22 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index f38a1972c0..bb8c3fca44 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.actors; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; 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.TenantId; 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.msg.TbActorMsg; 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.CalculatedFieldStateService; 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.edge.rpc.EdgeRpcService; 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.StringWriter; -import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; @Slf4j @Component @@ -842,7 +839,7 @@ public class ActorSystemContext { Futures.addCallback(future, RULE_CHAIN_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor()); } - public void persistCalculatedFieldDebugEvent(TenantId tenantId, CalculatedFieldId calculatedFieldId, EntityId entityId, Map 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)) { try { CalculatedFieldDebugEvent.CalculatedFieldDebugEventBuilder eventBuilder = CalculatedFieldDebugEvent.builder() @@ -855,13 +852,10 @@ public class ActorSystemContext { eventBuilder.msgId(tbMsgId); } if (tbMsgType != null) { - eventBuilder.msgType(tbMsgType.name()); + eventBuilder.msgType(tbMsgType); } if (arguments != null) { - eventBuilder.arguments(JacksonUtil.toString( - arguments.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().jsonValue())) - )); + eventBuilder.arguments(JacksonUtil.toString(arguments)); } if (result != null) { eventBuilder.result(result); diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 6cc1c50325..6b45552bac 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/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.stream.Collectors; +import static org.thingsboard.server.common.data.DataConstants.CF_REEVALUATION_MSG; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; /** @@ -351,7 +352,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } if (state.isSizeOk()) { 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, CF_REEVALUATION_MSG, msg.getCallback()); } else { throw new RuntimeException(ctx.getSizeExceedsLimitMessage()); } @@ -432,7 +433,8 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (!updatedArgs.isEmpty() || justRestored) { cfIdList = new ArrayList<>(cfIdList); 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 { callback.onSuccess(); } @@ -474,7 +476,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } private void processStateIfReady(CalculatedFieldState state, Map updatedArgs, CalculatedFieldCtx ctx, - List cfIdList, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { + List cfIdList, UUID tbMsgId, String tbMsgType, TbCallback callback) throws CalculatedFieldException { callback = new MultipleTbCallback(CALLBACKS_PER_CF, callback); log.trace("[{}][{}] Processing state if ready. Current args: {}, updated args: {}", entityId, ctx.getCfId(), state.getArguments(), updatedArgs); CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId); @@ -492,19 +494,19 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM callback.onSuccess(); } 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 { if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { 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(); } } catch (Exception 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 { if (!stateSizeChecked) { state.checkStateSize(ctxId, ctx.getMaxStateSize()); diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldException.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldException.java index 70c8dfbfd2..0242a33145 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldException.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldException.java @@ -15,14 +15,12 @@ */ package org.thingsboard.server.actors.calculatedField; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Builder; import lombok.Getter; 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 java.util.Map; import java.util.UUID; @Getter @@ -32,8 +30,8 @@ public class CalculatedFieldException extends Exception { private final CalculatedFieldCtx ctx; private final EntityId eventEntity; private final UUID msgId; - private final TbMsgType msgType; - private Map arguments; + private final String msgType; + private JsonNode arguments; private String errorMessage; private Exception cause; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 741c94c796..6df4240410 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/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; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Getter; import lombok.Setter; @@ -33,6 +34,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Getter public abstract class BaseCalculatedFieldState implements CalculatedFieldState, Closeable { @@ -185,4 +187,10 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, return ReadinessStatus.from(emptyArguments); } + @Override + public JsonNode getArgumentsJson() { + return JacksonUtil.valueToTree(arguments.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().jsonValue()))); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index cadd34c420..62163fecc2 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -646,7 +646,9 @@ public class CalculatedFieldCtx implements Closeable { } if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig && 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; } if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index e3914cc125..8a3c618254 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/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.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -59,6 +60,8 @@ public interface CalculatedFieldState extends Closeable { Map getArguments(); + JsonNode getArgumentsJson(); + long getLatestTimestamp(); void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java index 6ead645322..0fadc59b8c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -15,12 +15,15 @@ */ package org.thingsboard.server.service.cf.ctx.state.aggregation.single; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbUtils; +import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; @@ -36,6 +39,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.BaseCalculatedFieldState; 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.ZoneId; @@ -58,6 +62,8 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt private long checkInterval; private Map metrics; + private final EntityAggregationDebugArgumentsTracker debugTracker = new EntityAggregationDebugArgumentsTracker(new HashMap<>()); + private CalculatedFieldProcessingService cfProcessingService; public EntityAggregationCalculatedFieldState(EntityId entityId) { @@ -91,9 +97,14 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt @Override public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) throws Exception { + debugTracker.reset(); createIntervalIfNotExist(); long now = System.currentTimeMillis(); + if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { + debugTracker.recordUpdatedArgs(updatedArgs, arguments); + } + Map> results = new HashMap<>(); List expiredIntervals = new ArrayList<>(); getIntervals().forEach((intervalEntry, argIntervalStatuses) -> { @@ -114,6 +125,12 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt .build()); } + @Override + public Map update(Map argumentValues, CalculatedFieldCtx ctx) { + createIntervalIfNotExist(); + return super.update(argumentValues, ctx); + } + private void removeExpiredIntervals(List expiredIntervals) { expiredIntervals.forEach(expiredInterval -> { arguments.values().stream() @@ -183,6 +200,9 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt expiredIntervals.add(intervalEntry); } else if (now - startTs >= intervalEntry.getIntervalDuration()) { handleActiveInterval(intervalEntry, args, results); + if (watermarkDuration == 0) { + expiredIntervals.add(intervalEntry); + } } } @@ -262,14 +282,83 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt resultNode.put("ts", interval.getEndTs() - 1); resultNode.set("values", metricsNode); result.add(resultNode); + + if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { + debugTracker.addInterval(interval); + } } }); return result; } + @Override + public JsonNode getArgumentsJson() { + EntityAggregationDebugArguments debugArguments = debugTracker.toDebugArguments(); + return debugArguments == null ? null : JacksonUtil.valueToTree(debugArguments); + } + @Override public boolean isReady() { return true; } + record EntityAggregationDebugArgumentsTracker(Map> processedIntervals) { + + public void reset() { + processedIntervals.clear(); + } + + public void addInterval(AggIntervalEntry interval) { + processedIntervals.computeIfAbsent(interval, k -> new HashMap<>()); + } + + public void recordUpdatedArgs(Map updatedArgs, Map 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 processedIntervals) { + + public static EntityAggregationDebugArguments toDebugArguments(Map> processedIntervals) { + List result = new ArrayList<>(); + processedIntervals.forEach((interval, args) -> { + result.add(new IntervalDebugArgument(interval.getStartTs(), interval.getEndTs(), args)); + }); + return new EntityAggregationDebugArguments(result); + } + + } + + record IntervalDebugArgument(Long intervalStartTs, Long intervalEndTs, JsonNode updatedArguments) { + + public IntervalDebugArgument(Long intervalStartTs, Long intervalEndTs, Map updatedArguments) { + this(intervalStartTs, intervalEndTs, updatedArguments == null || updatedArguments.isEmpty() ? null : JacksonUtil.valueToTree(updatedArguments)); + } + + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 8a72b26a28..7830461109 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -105,6 +105,8 @@ public class DataConstants { public static final String RPC_FAILED = "RPC_FAILED"; public static final String RPC_DELETED = "RPC_DELETED"; + public static final String CF_REEVALUATION_MSG = "CF_REEVALUATION_MSG"; + public static final String DEFAULT_SECRET_KEY = ""; public static final String SECRET_KEY_FIELD_NAME = "secretKey"; public static final String DURATION_MS_FIELD_NAME = "durationMs"; From c0f8b87397916a377854bac310711d390d2e7a2d Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 25 Nov 2025 16:20:01 +0200 Subject: [PATCH 13/46] ignore null arguments --- .../single/EntityAggregationCalculatedFieldState.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java index 0fadc59b8c..dd67ec8123 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -15,6 +15,7 @@ */ 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.ObjectNode; @@ -353,6 +354,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt } + @JsonInclude(JsonInclude.Include.NON_NULL) record IntervalDebugArgument(Long intervalStartTs, Long intervalEndTs, JsonNode updatedArguments) { public IntervalDebugArgument(Long intervalStartTs, Long intervalEndTs, Map updatedArguments) { From 7fb7e3f24c1a08c750ce7c4c82932286d0d4feb6 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 25 Nov 2025 16:53:39 +0200 Subject: [PATCH 14/46] Refactoring - revert replaceUserCredentials. Added removeUserCredentials and use it instead --- .../rpc/processor/user/BaseUserProcessor.java | 35 +++--------- .../server/dao/user/UserService.java | 3 +- .../server/dao/user/UserServiceImpl.java | 56 +++++++------------ 3 files changed, 30 insertions(+), 64 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java index 0da0e4aade..fe640505b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -23,7 +23,6 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.service.DataValidator; @@ -90,40 +89,24 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { } protected void updateUserCredentials(TenantId tenantId, UserCredentialsUpdateMsg updateMsg) { - UserCredentials userCredentialsFromUpdateMsg = JacksonUtil.fromString(updateMsg.getEntity(), UserCredentials.class, true); - if (userCredentialsFromUpdateMsg == null) { + 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, userCredentialsFromUpdateMsg.getUserId()); + User user = edgeCtx.getUserService().findUserById(tenantId, userCredentials.getUserId()); if (user == null) { log.warn("[{}] Can't find user by id [{}] skipping credentials update. UserCredentialsUpdateMsg [{}]", - tenantId, userCredentialsFromUpdateMsg.getUserId(), updateMsg); + tenantId, userCredentials.getUserId(), updateMsg); return; } - log.debug("[{}] Updating user credentials for user [{}]. New credentials Id [{}], enabled [{}]", - tenantId, user.getName(), userCredentialsFromUpdateMsg.getId(), userCredentialsFromUpdateMsg.isEnabled()); - + tenantId, user.getName(), userCredentials.getId(), userCredentials.isEnabled()); try { - UserCredentials existing = edgeCtx.getUserService().findUserCredentialsByUserId(tenantId, user.getId()); - boolean created = existing == null; - UserCredentialsId oldCredentialsId = created ? null : existing.getId(); - - UserCredentials updated = created ? new UserCredentials() : existing; - updated.setId(userCredentialsFromUpdateMsg.getId()); - updated.setUserId(user.getId()); - updated.setEnabled(userCredentialsFromUpdateMsg.isEnabled()); - updated.setActivateToken(userCredentialsFromUpdateMsg.getActivateToken()); - updated.setAdditionalInfo(userCredentialsFromUpdateMsg.getAdditionalInfo()); - updated.setPassword(userCredentialsFromUpdateMsg.getPassword()); - updated.setResetToken(userCredentialsFromUpdateMsg.getResetToken()); - - if (created) { - edgeCtx.getUserService().saveUserCredentials(tenantId, updated, false); - } else { - edgeCtx.getUserService().replaceUserCredentials(tenantId, updated, oldCredentialsId, false); + 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); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index c3f7b72ed3..1062982885 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -74,8 +74,7 @@ public interface UserService extends EntityDaoService { UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials); - UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials, - UserCredentialsId oldUserCredentialsId, boolean doValidate); + void deleteUserCredentials(TenantId tenantId, UserCredentials userCredentials); void deleteUser(TenantId tenantId, User user); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index bdcac80d4a..e10d87e760 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -79,7 +79,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -99,6 +98,7 @@ public class UserServiceImpl extends AbstractCachedEntityService tenantId); + userCredentialsDao.removeById(tenantId, userCredentials.getUuidId()); + userCredentials.setId(null); + if (userCredentials.getPassword() != null) { + updatePasswordHistory(userCredentials); + } + UserCredentials result = userCredentialsDao.save(tenantId, userCredentials); + eventPublisher.publishEvent(ActionEntityEvent.builder() + .tenantId(tenantId) + .entityId(userCredentials.getUserId()) + .actionType(ActionType.CREDENTIALS_UPDATED).build()); + return result; } @Override - public UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials, - UserCredentialsId oldUserCredentialsId, boolean doValidate) { - return replaceUserCredentialsInternal(tenantId, userCredentials, oldUserCredentialsId.getId(), doValidate); - } - - private UserCredentials replaceUserCredentialsInternal(TenantId tenantId, UserCredentials userCredentials, - UUID oldCredentialsUuid, boolean doValidate) { - log.trace("[{}] Replacing user credentials for user [{}], old credentials ID [{}]", - tenantId, userCredentials.getUserId(), oldCredentialsUuid); - - if (doValidate) { - userCredentialsValidator.validate(userCredentials, data -> tenantId); - } - - try { - userCredentialsDao.removeById(tenantId, oldCredentialsUuid); - userCredentials.setId(null); - if (userCredentials.getPassword() != null) { - updatePasswordHistory(userCredentials); - } - - UserCredentials savedCredentials = userCredentialsDao.save(tenantId, userCredentials); - - eventPublisher.publishEvent(ActionEntityEvent.builder() - .tenantId(tenantId) - .entityId(userCredentials.getUserId()) - .actionType(ActionType.CREDENTIALS_UPDATED) - .build()); - - return savedCredentials; - } catch (Exception e) { - log.error("[{}] Failed to replace user credentials for user [{}]", tenantId, userCredentials.getUserId(), e); - throw new RuntimeException("Failed to replace user credentials", e); - } + 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 From c5a8a874a97e20c67db49ea12b948cfb4d8c508e Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 25 Nov 2025 17:00:29 +0200 Subject: [PATCH 15/46] refacotring to be in sync with PE --- .../edge/rpc/processor/user/BaseUserProcessor.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java index fe640505b1..a742d83ada 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -47,8 +47,12 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { } User userById = edgeCtx.getUserService().findUserById(tenantId, userId); - isCreated = userById == null; - user.setId(isCreated ? null : 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()); From cfb596f6d69a5ddb6bb4d3e2aa2133ac5ec7db37 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 26 Nov 2025 08:29:11 +0200 Subject: [PATCH 16/46] added check for cf exception to avoid empty event in debug --- .../calculatedField/CalculatedFieldEntityMessageProcessor.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 6b45552bac..27206dc614 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -312,6 +312,9 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } catch (Exception 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(); } } From d6315b33293e8dd357cf4dc747b1ac4b5a932b4e Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 26 Nov 2025 12:40:22 +0200 Subject: [PATCH 17/46] updated related entities debug arguments structure --- ...titiesAggregationCalculatedFieldState.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java index 28d7457c1f..dfffcb3777 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java +++ b/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; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -23,6 +24,7 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; 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.configuration.Output; 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.RelatedEntitiesAggregationCalculatedFieldConfiguration; 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.TelemetryCalculatedFieldResult; 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.CalculatedFieldCtx; 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.Entry; import java.util.concurrent.ScheduledFuture; +import java.util.stream.Collectors; import static java.util.concurrent.TimeUnit.SECONDS; @@ -62,6 +67,8 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat private ScheduledFuture reevaluationFuture; + private EntityService entityService; + public RelatedEntitiesAggregationCalculatedFieldState(EntityId entityId) { super(entityId); } @@ -72,6 +79,7 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); metrics = configuration.getMetrics(); deduplicationIntervalMs = SECONDS.toMillis(configuration.getDeduplicationIntervalInSec()); + entityService = ctx.getSystemContext().getEntityService(); } @Override @@ -247,4 +255,22 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat } } + @Override + public JsonNode getArgumentsJson() { + List entitiesArguments = new ArrayList<>(); + prepareInputs().forEach((entityId, entityArguments) -> { + entityService.fetchEntityName(ctx.getTenantId(), entityId).ifPresent(entityName -> { + EntityInfo entityInfo = new EntityInfo(entityId, entityName); + 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 entitiesArguments) {} + + record EntityArgument(EntityInfo entity, JsonNode entityArguments) {} + } From 7921162d337d82a31a93cc8d6e017f90e670bcee Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 26 Nov 2025 15:52:06 +0200 Subject: [PATCH 18/46] cleanup --- .../login/create-password.component.html | 2 +- .../pages/login/create-password.component.ts | 37 ++++++------------- .../pages/login/reset-password.component.html | 2 +- .../pages/login/reset-password.component.ts | 35 ++++++------------ .../src/app/shared/models/password.models.ts | 24 ++---------- .../assets/locale/locale.constant-en_US.json | 6 --- 6 files changed, 30 insertions(+), 76 deletions(-) diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html index 79b601d099..e3e43b6d29 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html @@ -42,7 +42,7 @@ formControlName="newPassword"/> lock - + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts index e7aa68cba0..ec8a0efa0c 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts @@ -16,15 +16,11 @@ import { Component } from '@angular/core'; 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 { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { TranslateService } from '@ngx-translate/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { UserPasswordPolicy } from '@shared/models/settings.models'; -import { combineLatest } from 'rxjs'; import { passwordsMatchValidator, passwordStrengthValidator @@ -37,26 +33,21 @@ import { }) export class CreatePasswordComponent extends PageComponent { - activateToken = ''; - createPassword: UntypedFormGroup; passwordPolicy: UserPasswordPolicy; + createPassword: FormGroup; - constructor(protected store: Store, - private route: ActivatedRoute, + private activateToken: string; + + constructor(private route: ActivatedRoute, private authService: AuthService, - private translate: TranslateService, - private fb: UntypedFormBuilder) { - super(store); + private fb: FormBuilder) { + super(); + + this.activateToken = this.route.snapshot.queryParams['activateToken'] || ''; - combineLatest([ - this.route.queryParams, - this.route.data - ]) + this.route.data .pipe(takeUntilDestroyed()) - .subscribe(([params, data]) => { - this.activateToken = params['activateToken'] || ''; - this.passwordPolicy = data['passwordPolicy']; - }); + .subscribe((data) => this.passwordPolicy = data['passwordPolicy']); this.buildCreatePasswordForm(); } @@ -72,17 +63,13 @@ export class CreatePasswordComponent extends PageComponent { }); } - get passwordErrorsLength(): number { - return Object.keys(this.createPassword.get('newPassword')?.errors ?? {}).length; - } - onCreatePassword() { if (this.createPassword.invalid) { this.createPassword.markAllAsTouched(); } else { this.authService.activate( this.activateToken, - this.createPassword.get('password').value, true).subscribe(); + this.createPassword.get('newPassword').value, true).subscribe(); } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html index b4be2ecc9d..95584f9a75 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html @@ -45,7 +45,7 @@ formControlName="newPassword"/> lock - + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts index 4a2c0bc75d..bfc8986625 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts @@ -16,13 +16,9 @@ import { Component } from '@angular/core'; 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 { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { TranslateService } from '@ngx-translate/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { combineLatest } from 'rxjs'; import { UserPasswordPolicy } from '@shared/models/settings.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { @@ -39,25 +35,22 @@ export class ResetPasswordComponent extends PageComponent { isExpiredPassword: boolean; - resetToken = ''; - - resetPassword: UntypedFormGroup; + resetPassword: FormGroup; passwordPolicy: UserPasswordPolicy; - constructor(protected store: Store, - private route: ActivatedRoute, + private resetToken: string; + + constructor(private route: ActivatedRoute, private router: Router, private authService: AuthService, - private translate: TranslateService, - private fb: UntypedFormBuilder) { - super(store); - combineLatest([ - this.route.queryParams, - this.route.data - ]) + private fb: FormBuilder) { + super(); + + this.resetToken = this.route.snapshot.queryParams['resetToken'] || ''; + + this.route.data .pipe(takeUntilDestroyed()) - .subscribe(([params, data]) => { - this.resetToken = params['resetToken'] || ''; + .subscribe((data) => { this.passwordPolicy = data['passwordPolicy']; this.isExpiredPassword = data['expiredPassword'] ?? false; }); @@ -76,10 +69,6 @@ export class ResetPasswordComponent extends PageComponent { }); } - get passwordErrorsLength(): number { - return Object.keys(this.resetPassword.get('newPassword')?.errors ?? {}).length; - } - onResetPassword() { if (this.resetPassword.invalid) { this.resetPassword.markAllAsTouched(); diff --git a/ui-ngx/src/app/shared/models/password.models.ts b/ui-ngx/src/app/shared/models/password.models.ts index 4bba3d6737..dbffb35e92 100644 --- a/ui-ngx/src/app/shared/models/password.models.ts +++ b/ui-ngx/src/app/shared/models/password.models.ts @@ -18,24 +18,14 @@ import { UserPasswordPolicy } from '@shared/models/settings.models'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; import { isEqual } from '@core/utils'; -export enum PasswordErrorMessageKey { - minLength = 'security.password-requirement.password-min-length', - maxLength = 'security.password-requirement.password-max-length', - notUpperCase = 'security.password-requirement.password-uppercase', - notLowerCase = 'security.password-requirement.password-lowercase', - notNumeric = 'security.password-requirement.password-digit', - notSpecial = 'security.password-requirement.password-special-characters', - hasWhitespaces = 'security.password-requirement.password-should-not-contain-spaces', - default = 'security.password-requirement.password-not-meet-requirements' -} - 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' + notSpecial = 'security.password-requirement.password-tooltip-special-characters', + hasWhitespaces = 'security.password-requirement.password-should-not-contain-spaces' } export const passwordErrorRules = [ @@ -45,6 +35,7 @@ export const passwordErrorRules = [ { 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 =>{ @@ -59,14 +50,7 @@ export const passwordsMatchValidator = (firstControlName: string, secondControlN const newPass = newPassControl.value ?? ''; const confirm = confirmControl.value ?? ''; - const userInteracted = - confirmControl.touched || confirmControl.dirty || group.touched; - - if (!userInteracted) { - return null; - } - - if (newPass && confirm !== newPass) { + if ((newPass || confirm) && confirm !== newPass) { confirmControl.setErrors({ passwordsNotMatch: true }); return { passwordsNotMatch: true }; } else { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 8f3ef7e3d8..ad8d0aa54f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4418,12 +4418,6 @@ "password-tooltip-lowercase": "{{minimumLowercaseLetters}} lowercase character", "password-tooltip-digit": "{{minimumDigits}} number", "password-tooltip-special-characters": "{{minimumSpecialCharacters}} special character", - "password-min-length": "Password must be {{minimumLength}} or more characters in length", - "password-max-length": "Password should be less than {{maximumLength}}", - "password-uppercase": "Password must contain {{minimumUppercaseLetters}} or more uppercase characters", - "password-lowercase": "Password must contain {{minimumLowercaseLetters}} or more lowercase characters", - "password-digit": "Password must contain {{minimumDigits}} or more digit characters", - "password-special-characters": "Password must contain {{minimumSpecialCharacters}} or more special characters", "incorrect-password-try-again": "Incorrect password. Try again", "lowercase-letter": "{ count, plural, =1 {1 lowercase letter} other {# lowercase letters} }", "new-passwords-not-match": "New password didn't match", From 1d9892c9783ada1823905c4df0e644b066682ab5 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 26 Nov 2025 16:20:51 +0200 Subject: [PATCH 19/46] changed ViewEncapsulation cleanup --- .../modules/login/pages/login/create-password.component.ts | 2 +- .../components/password-requirements-tooltip.component.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts index ec8a0efa0c..6e3dcc0f63 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts @@ -55,7 +55,7 @@ export class CreatePasswordComponent extends PageComponent { private buildCreatePasswordForm() { this.createPassword = this.fb.group({ newPassword: ['', [Validators.required, passwordStrengthValidator(this.passwordPolicy)]], - newPassword2:[''] + newPassword2: [''] }, { validators: [ passwordsMatchValidator('newPassword', 'newPassword2'), diff --git a/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts index 9d8c4d9b66..e279708184 100644 --- a/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts +++ b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Input } from '@angular/core'; +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'; @@ -23,7 +23,8 @@ import { UserPasswordPolicy } from '@shared/models/settings.models'; @Component({ selector: 'tb-password-requirements-tooltip', templateUrl: './password-requirements-tooltip.component.html', - styleUrl: './password-requirements-tooltip.component.scss' + styleUrl: './password-requirements-tooltip.component.scss', + encapsulation: ViewEncapsulation.None }) export class PasswordRequirementsTooltipComponent { @Input() passwordControl: AbstractControl; From c35288af5fadbbbb779dc1b0053db56d22f86d45 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Wed, 26 Nov 2025 19:27:09 +0200 Subject: [PATCH 20/46] Refactor deleteUserAndPushEntityDeletedEventToRuleEngine to add additional metadata --- .../rpc/processor/user/BaseUserProcessor.java | 34 ++++++++++++++++++- .../rpc/processor/user/UserEdgeProcessor.java | 11 ------ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java index a742d83ada..fc24588f5b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -21,10 +21,14 @@ 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; @@ -82,7 +86,23 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { return Pair.of(isCreated, userEmailUpdated); } - protected User deleteUser(TenantId tenantId, UserId userId) { + 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); @@ -118,6 +138,18 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { } } + 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); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index 968d573618..3b2d76356b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -120,17 +120,6 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso } } - private 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 = getEdgeActionTbMsgMetaData(edge, userCustomerId); - pushEntityEventToRuleEngine(tenantId, userId, userCustomerId, TbMsgType.ENTITY_DELETED, userAsString, msgMetaData); - } - @Override public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { UserId userId = new UserId(edgeEvent.getEntityId()); From 09d83b2df35a6924dde70745ef57c938c61f2951 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Thu, 27 Nov 2025 10:06:54 +0200 Subject: [PATCH 21/46] UI: Fixed Alarm Rule validation --- .../alarm-rule-dialog.component.ts | 16 +------------ .../alarm-rules/alarm-rules-table-config.ts | 2 +- .../cf-alarm-rule-condition.component.ts | 10 ++++---- .../alarm-rules/cf-alarm-rule.component.ts | 23 +++++++++---------- .../create-cf-alarm-rules.component.ts | 6 ++--- 5 files changed, 21 insertions(+), 36 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts index 2209cbafba..f45e205246 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts @@ -67,7 +67,7 @@ export class AlarmRuleDialogComponent extends DialogComponent(null, Validators.required), - id: ['', Validators.required], + id: [null as null | string, Validators.required], }), configuration: this.fb.group({ arguments: this.fb.control({}), @@ -101,7 +101,6 @@ export class AlarmRuleDialogComponent extends DialogComponent { - if (loading) { - this.fieldFormGroup.disable({emitEvent: false}); - } else { - this.fieldFormGroup.enable({emitEvent: false}); - if (this.data.isDirty) { - this.fieldFormGroup.markAsDirty(); - } - } - }); - } - onTestScript(expression: string): Observable { const calculatedFieldId = this.data.value?.id?.id; if (calculatedFieldId) { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index 6fa6515663..8593b28956 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -152,7 +152,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { this.cellActionDescriptors.push( { - name: this.translate.instant('notification.copy-template'), + name: this.translate.instant('alarm-rule.copy'), icon: 'content_copy', isEnabled: () => true, onAction: ($event, entity) => this.copyCalculatedField(entity) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts index 2a0b7a7810..543b0a86dc 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts @@ -20,7 +20,7 @@ import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormControl, + ValidationErrors, Validator } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; @@ -130,10 +130,10 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali } 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 : { alarmRuleCondition: { valid: false, @@ -166,11 +166,11 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali private updateConditionInfo() { this.alarmRuleConditionFormGroup.patchValue( - { + this.modelValue ? { type: this.modelValue?.type, expression: this.modelValue?.expression, schedule: this.modelValue?.schedule, - }, {emitEvent: false} + } : null, {emitEvent: false} ); this.updateScheduleText(); this.updateSpecText(); diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts index 5db525074d..cd6e06f293 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts @@ -20,8 +20,9 @@ import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormControl, - Validator + ValidationErrors, + Validator, + Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { isDefinedAndNotNull } from '@core/utils'; @@ -76,7 +77,7 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid private modelValue: AlarmRule; alarmRuleFormGroup = this.fb.group({ - condition: this.fb.control(null), + condition: this.fb.control(null, Validators.required), alarmDetails: [null], dashboardId: [null] }); @@ -113,14 +114,12 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid } writeValue(value: AlarmRule): void { - if (value) { - this.modelValue = value; - const model = this.modelValue ? { - ...this.modelValue, - dashboardId: this.modelValue.dashboardId?.id - } : null; - this.alarmRuleFormGroup.patchValue(model, {emitEvent: false}); - } + this.modelValue = value; + const model = this.modelValue ? { + ...this.modelValue, + dashboardId: this.modelValue.dashboardId?.id + } : null; + this.alarmRuleFormGroup.patchValue(model, {emitEvent: false}); } 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 : { alarmRule: { valid: false, diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts index 2197d6d14c..8959d709fa 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts @@ -23,7 +23,7 @@ import { NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormArray, - UntypedFormControl, + ValidationErrors, Validator, Validators } from '@angular/forms'; @@ -159,8 +159,8 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida return null; } - public validate(c: UntypedFormControl) { - return this.createAlarmRulesFormArray().length ? null : { + public validate(): ValidationErrors | null { + return this.createAlarmRulesFormGroup.valid && this.createAlarmRulesFormArray().length > 0 ? null : { createAlarmRules: { valid: false, }, From 85224c8b661002644135773ab4c2b26d4f822043 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 27 Nov 2025 12:04:40 +0200 Subject: [PATCH 22/46] fixed reset of audit log filter reset --- .../home/components/audit-log/audit-log-filter.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts index 41d6838120..650f444690 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts +++ b/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() { if (this.initialAuditLogFilter) { - if (!auditLogFilterEquals(this.auditLogFilter, this.initialAuditLogFilter)) { + if (!auditLogFilterEquals(this.auditLogFilterForm.getRawValue(), this.initialAuditLogFilter)) { this.updateAuditLogFilterForm(this.initialAuditLogFilter); this.auditLogFilterForm.markAsDirty(); } From fc3e7d69d8c88f82cfc3df10a0de2cc0d03eeaf0 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 27 Nov 2025 12:20:43 +0200 Subject: [PATCH 23/46] fixed displaying of disabled data --- .../home/components/widget/widget-config.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index c23619f5c0..87951d263b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -934,15 +934,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe entityLabelColumnTitle } = this.modelValue.config.settings; const displayEntitiesArray = []; - if (isDefined(displayEntityName)) { + if (displayEntityName) { const displayName = entityNameColumnTitle ? entityNameColumnTitle : 'entityName'; displayEntitiesArray.push({name: displayName, label: displayName}); } - if (isDefined(displayEntityLabel)) { + if (displayEntityLabel) { const displayLabel = entityLabelColumnTitle ? entityLabelColumnTitle : 'entityLabel'; displayEntitiesArray.push({name: displayLabel, label: displayLabel}); } - if (isDefined(displayEntityType)) { + if (displayEntityType) { displayEntitiesArray.push({name: 'entityType', label: 'entityType'}); } configuredColumns.push(...displayEntitiesArray, ...this.keysToCellClickColumns(this.modelValue.config.datasources[0].dataKeys)); From 7ec59138e8a227d4fbf5c5f794ee5b664a3edfb5 Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Thu, 27 Nov 2025 13:21:16 +0200 Subject: [PATCH 24/46] Dashboard timewindow: remove unused parameters on creating/editing dashboard --- ui-ngx/src/app/core/services/dashboard-utils.service.ts | 5 ++--- .../home/components/dashboard/dashboard.component.ts | 7 +++---- ui-ngx/src/app/shared/models/time/time.models.ts | 5 +++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index b630811f93..a23dd04337 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -168,9 +168,8 @@ export class DashboardUtilsService { dashboard.configuration.filters = {}; } - if (isUndefined(dashboard.configuration.timewindow)) { - dashboard.configuration.timewindow = this.timeService.defaultTimewindow(true); - } + dashboard.configuration.timewindow = initModelFromDefaultTimewindow(dashboard.configuration.timewindow, + false, false, this.timeService, true, true); if (isUndefined(dashboard.configuration.settings)) { dashboard.configuration.settings = {}; dashboard.configuration.settings.stateControllerId = 'entity'; diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts index 91888d9b5a..75a229c4a1 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts +++ b/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 { AuthUser } from '@shared/models/user.model'; 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 { GridsterComponent, GridsterConfig, GridType } from 'angular-gridster2'; import { @@ -223,9 +223,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo ngOnInit(): void { this.dashboardWidgets.parentDashboard = this.parentDashboard; this.dashboardWidgets.popoverComponent = this.popoverComponent; - if (!this.dashboardTimewindow) { - this.dashboardTimewindow = this.timeService.defaultTimewindow(true); - } + this.dashboardTimewindow = initModelFromDefaultTimewindow(this.dashboardTimewindow, + false, false, this.timeService, true, true); this.gridsterOpts = { gridType: this.gridType || GridType.ScrollVertical, keepFixedHeightInMobile: true, diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index 646e7bdc64..1046feb2ca 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/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, - historyOnly: boolean, timeService: TimeService, hasAggregation: boolean): Timewindow => { - const model = defaultTimewindow(timeService); + historyOnly: boolean, timeService: TimeService, hasAggregation: boolean, + isDashboard = false): Timewindow => { + const model = defaultTimewindow(timeService, isDashboard); if (value) { if (value.allowedAggTypes?.length) { model.allowedAggTypes = value.allowedAggTypes; From f9393ba52cb20b34d150751236701d678844868f Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 27 Nov 2025 15:12:29 +0200 Subject: [PATCH 25/46] UI: Redesign login page OAuth2 providers --- .../oauth2/clients/client.component.html | 14 ++++--- .../domains/domain-table-config.resolver.ts | 4 +- .../login/pages/login/login.component.html | 40 +++++++++++++------ .../login/pages/login/login.component.scss | 36 +++++++++++++---- 4 files changed, 68 insertions(+), 26 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html index 60a837c583..37a5c3b464 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html @@ -136,7 +136,7 @@ -
+
admin.oauth2.login-button-label - - admin.oauth2.login-button-icon - - +
+
admin.oauth2.login-button-icon
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts index 4b3c57f942..aa3f3fc7dc 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts +++ b/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.saveEntity = (domain, originalDomain) => { const clientsIds = domain.oauth2ClientInfos as Array || []; - const shouldUpdateClients = domain.id && !isEqual(domain.oauth2ClientInfos?.sort(), - originalDomain.oauth2ClientInfos?.map(info => info.id ? info.id.id : info).sort()); + const shouldUpdateClients = domain.id && !isEqual(domain.oauth2ClientInfos, + originalDomain.oauth2ClientInfos?.map(info => info.id ? info.id.id : info)); delete domain.oauth2ClientInfos; return this.domainService.saveDomain(domain, domain.id ? null : clientsIds).pipe( diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.html b/ui-ngx/src/app/modules/login/pages/login/login.component.html index ad95027958..94350c80f0 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.html @@ -28,19 +28,35 @@
-
- - - -
-
-
{{ "login.or" | translate | uppercase }}
-
+ @if(oauth2Clients?.length) { +
+ @if(oauth2Clients.length === 1) { + + } @else { + + } +
+
+
{{ "login.or" | translate | uppercase }}
+
+
-
+ } login.username diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.scss b/ui-ngx/src/app/modules/login/pages/login/login.component.scss index 2784335d7b..8859aed7e4 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.scss @@ -59,8 +59,11 @@ } .text { - padding-right: 10px; - padding-left: 10px; + padding-right: 8px; + padding-left: 8px; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.15px; } } @@ -72,14 +75,33 @@ a.login-with-button { color: rgba(black, 0.87); 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 { - border-bottom: 0; + &:has(> :nth-child(2):last-child) { + a.login-with-button { + max-width: 180px; + flex-basis: 180px; + } } - .icon { - height: 20px; - width: 20px; + &:has(> :nth-child(3):last-child) { + a.login-with-button { + max-width: 180px; + } } } } From a164e074b373b7af385cc8087f33962818b2a4c3 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 27 Nov 2025 16:18:32 +0200 Subject: [PATCH 26/46] get all entity infos instead of getting for each separately --- ...CalculatedFieldEntityMessageProcessor.java | 4 ++-- ...titiesAggregationCalculatedFieldState.java | 14 +++++++------- ...EntityAggregationCalculatedFieldState.java | 19 ++++++++++++------- .../server/common/data/DataConstants.java | 2 +- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 27206dc614..9fe0698f88 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -70,7 +70,7 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.DataConstants.CF_REEVALUATION_MSG; +import static org.thingsboard.server.common.data.DataConstants.REEVALUATION_MSG; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; /** @@ -355,7 +355,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } if (state.isSizeOk()) { log.debug("[{}][{}] Reevaluating CF state", entityId, cfId); - processStateIfReady(state, null, ctx, Collections.singletonList(cfId), null, CF_REEVALUATION_MSG, msg.getCallback()); + processStateIfReady(state, null, ctx, Collections.singletonList(cfId), null, REEVALUATION_MSG, msg.getCallback()); } else { throw new RuntimeException(ctx.getSizeExceedsLimitMessage()); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java index dfffcb3777..3b4092a885 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java @@ -257,14 +257,14 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat @Override public JsonNode getArgumentsJson() { + Map> inputs = prepareInputs(); + Map entityIdEntityInfos = entityService.fetchEntityInfos(ctx.getTenantId(), null, inputs.keySet()); List entitiesArguments = new ArrayList<>(); - prepareInputs().forEach((entityId, entityArguments) -> { - entityService.fetchEntityName(ctx.getTenantId(), entityId).ifPresent(entityName -> { - EntityInfo entityInfo = new EntityInfo(entityId, entityName); - JsonNode entityArgumentsJson = JacksonUtil.valueToTree(entityArguments.entrySet().stream() - .collect(Collectors.toMap(Entry::getKey, e -> e.getValue().jsonValue()))); - entitiesArguments.add(new EntityArgument(entityInfo, entityArgumentsJson)); - }); + inputs.forEach((entityId, entityArguments) -> { + EntityInfo entityInfo = entityIdEntityInfos.get(entityId); + 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)); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java index dd67ec8123..17aa90f099 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import org.apache.commons.lang3.concurrent.LazyInitializer; import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbUtils; @@ -63,7 +64,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt private long checkInterval; private Map metrics; - private final EntityAggregationDebugArgumentsTracker debugTracker = new EntityAggregationDebugArgumentsTracker(new HashMap<>()); + private EntityAggregationDebugArgumentsTracker debugTracker; private CalculatedFieldProcessingService cfProcessingService; @@ -98,11 +99,14 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt @Override public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) throws Exception { - debugTracker.reset(); createIntervalIfNotExist(); long now = System.currentTimeMillis(); if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { + LazyInitializer lazy = LazyInitializer.builder() + .setInitializer(() -> new EntityAggregationDebugArgumentsTracker(new HashMap<>())) + .get(); + debugTracker = lazy.get(); debugTracker.recordUpdatedArgs(updatedArgs, arguments); } @@ -285,7 +289,9 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt result.add(resultNode); if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { - debugTracker.addInterval(interval); + if (debugTracker != null) { + debugTracker.addInterval(interval); + } } } }); @@ -294,6 +300,9 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt @Override public JsonNode getArgumentsJson() { + if (debugTracker == null) { + return null; + } EntityAggregationDebugArguments debugArguments = debugTracker.toDebugArguments(); return debugArguments == null ? null : JacksonUtil.valueToTree(debugArguments); } @@ -305,10 +314,6 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt record EntityAggregationDebugArgumentsTracker(Map> processedIntervals) { - public void reset() { - processedIntervals.clear(); - } - public void addInterval(AggIntervalEntry interval) { processedIntervals.computeIfAbsent(interval, k -> new HashMap<>()); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 7830461109..913b8170ae 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -105,7 +105,7 @@ public class DataConstants { public static final String RPC_FAILED = "RPC_FAILED"; public static final String RPC_DELETED = "RPC_DELETED"; - public static final String CF_REEVALUATION_MSG = "CF_REEVALUATION_MSG"; + public static final String REEVALUATION_MSG = "REEVALUATION_MSG"; public static final String DEFAULT_SECRET_KEY = ""; public static final String SECRET_KEY_FIELD_NAME = "secretKey"; From 66936c48ab72768029c83f59662a2cef81425f6f Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 27 Nov 2025 16:39:09 +0200 Subject: [PATCH 27/46] removed lazy initializer --- ...tedEntitiesAggregationCalculatedFieldState.java | 8 +++++--- .../EntityAggregationCalculatedFieldState.java | 14 +++++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java index 3b4092a885..264f651eb0 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java @@ -262,9 +262,11 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat List entitiesArguments = new ArrayList<>(); inputs.forEach((entityId, entityArguments) -> { EntityInfo entityInfo = entityIdEntityInfos.get(entityId); - JsonNode entityArgumentsJson = JacksonUtil.valueToTree(entityArguments.entrySet().stream() - .collect(Collectors.toMap(Entry::getKey, e -> e.getValue().jsonValue()))); - entitiesArguments.add(new EntityArgument(entityInfo, entityArgumentsJson)); + 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)); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java index 17aa90f099..f3c3e8a1cc 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import org.apache.commons.lang3.concurrent.LazyInitializer; import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbUtils; @@ -103,10 +102,11 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt long now = System.currentTimeMillis(); if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { - LazyInitializer lazy = LazyInitializer.builder() - .setInitializer(() -> new EntityAggregationDebugArgumentsTracker(new HashMap<>())) - .get(); - debugTracker = lazy.get(); + if (debugTracker == null) { + debugTracker = new EntityAggregationDebugArgumentsTracker(new HashMap<>()); + } else { + debugTracker.reset(); + } debugTracker.recordUpdatedArgs(updatedArgs, arguments); } @@ -314,6 +314,10 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt record EntityAggregationDebugArgumentsTracker(Map> processedIntervals) { + public void reset() { + processedIntervals.clear(); + } + public void addInterval(AggIntervalEntry interval) { processedIntervals.computeIfAbsent(interval, k -> new HashMap<>()); } From 7f73097f9011571607ff574373948f0a23a96dd6 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 27 Nov 2025 16:41:41 +0200 Subject: [PATCH 28/46] UI: Mark required field description in API key --- .../components/api-key/add-api-key-dialog.component.html | 5 ++++- .../api-key/edit-api-key-description-panel.component.html | 5 ++++- .../api-key/edit-api-key-description-panel.component.ts | 4 ++-- ui-ngx/src/assets/locale/locale.constant-en_US.json | 1 + 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html b/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html index dd91707449..defcdbb81d 100644 --- a/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html @@ -35,7 +35,10 @@
api-key.description - + + + {{ 'asset.description-required' | translate }} + {{ 'api-key.enable' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html index 11f9e0e657..bd889248c9 100644 --- a/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html @@ -21,7 +21,10 @@ api-key.description - {{input.value?.length || 0}}/255 + + {{ 'asset.description-required' | translate }} + + {{ input.value?.length || 0 }}/255