diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index ce7ea1efdd..58b1e8d422 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.exception; +import jakarta.persistence.PersistenceException; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -52,6 +53,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; +import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; @@ -162,8 +164,8 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand handleAuthenticationException(authenticationException, response); } else if (exception instanceof MaxPayloadSizeExceededException maxPayloadSizeExceededException) { handleMaxPayloadSizeExceededException(response, maxPayloadSizeExceededException); - } else if (exception instanceof DataAccessException e) { - handleDatabaseException(e, response); + } else if (exception instanceof DataAccessException || exception instanceof PersistenceException) { + handleDatabaseException(exception, response); } else { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(exception.getMessage(), @@ -217,8 +219,17 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand private void handleDatabaseException(Throwable databaseException, HttpServletResponse response) throws IOException { ThingsboardErrorResponse errorResponse; - if (databaseException instanceof ConstraintViolationException) { - errorResponse = ThingsboardErrorResponse.of(ExceptionUtils.getRootCause(databaseException).getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST); + ConstraintViolationException constraintViolationException = DaoUtil.extractConstraintViolation(databaseException); + if (constraintViolationException != null) { + log.debug("Constraint violation: {}", ExceptionUtils.getRootCauseMessage(databaseException)); + String constraintName = constraintViolationException.getConstraintName(); + String userMessage; + if (constraintName != null && !constraintName.isEmpty()) { + userMessage = "Constraint violation: " + constraintName; + } else { + userMessage = "Constraint violation"; + } + errorResponse = ThingsboardErrorResponse.of(userMessage, ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST); } else { log.warn("Database error: {} - {}", databaseException.getClass().getSimpleName(), ExceptionUtils.getRootCauseMessage(databaseException)); errorResponse = ThingsboardErrorResponse.of("Database error", ThingsboardErrorCode.DATABASE, HttpStatus.INTERNAL_SERVER_ERROR); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 0ff227384e..6a658ffd66 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -1162,7 +1162,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { assertThat(findRelationsByTo(entityTo)).hasSize(1); doDelete(urlDelete) - .andExpect(status().isInternalServerError()); + .andExpect(status().isBadRequest()); assertThat(findRelationsByTo(entityTo)).hasSize(1); } finally { diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java index fb86ea7b43..aeca72c739 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java @@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.TenantNotFoundException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -936,6 +937,26 @@ public class TenantControllerTest extends AbstractControllerTest { Mockito.reset(tbClusterService); } + @Test + public void testSaveTenantWithNonExistentTenantProfileId() throws Exception { + loginSysAdmin(); + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + tenant.setTenantProfileId(new TenantProfileId(UUID.randomUUID())); + + String responseBody = doPost("/api/tenant", tenant) + .andExpect(status().isBadRequest()) + .andReturn().getResponse().getContentAsString(); + + // Verify sanitized message format + assertThat(responseBody).contains("Constraint violation: fk_tenant_profile"); + // Verify raw SQL details are not returned + assertThat(responseBody).doesNotContain("could not execute statement"); + assertThat(responseBody).doesNotContain("insert or update on table"); + assertThat(responseBody).doesNotContain("tenant_profile_id"); + assertThat(responseBody).doesNotContain("is not present in table"); + } + private void testBroadcastEntityStateChangeEventNeverTenant() { Mockito.verify(tbClusterService, never()).onTenantChange(Mockito.any(Tenant.class), Mockito.isNull()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index a19ba6c0e7..4b469851cd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -202,4 +203,13 @@ public final class DaoUtil { .collect(Collectors.toList()); } + public static ConstraintViolationException extractConstraintViolation(Throwable t) { + if (t instanceof ConstraintViolationException cve) { + return cve; + } else if (t != null && t.getCause() instanceof ConstraintViolationException cve) { + return cve; + } + return null; + } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html index 4f95435f35..083178ddb3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html @@ -45,8 +45,7 @@ {{ 'widgets.maps.overlays.markers' | translate }} {{ 'widgets.maps.overlays.polygons' | translate }} {{ 'widgets.maps.overlays.circles' | translate }} - //todo translation - Polylines + {{ 'widgets.maps.overlays.polylines' | translate }}