diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index a58ae911a2..83603f7ea5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -151,7 +151,7 @@ import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.exception.EntitiesLimitException; +import org.thingsboard.server.dao.exception.EntitiesLimitExceededException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.mobile.MobileAppBundleService; @@ -453,7 +453,7 @@ public abstract class BaseController { } if (exception instanceof ThingsboardException) { return (ThingsboardException) exception; - } else if (exception instanceof EntitiesLimitException) { + } else if (exception instanceof EntitiesLimitExceededException) { return new ThingsboardException(exception, ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED); } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardEntitiesLimitExceededResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardEntitiesLimitExceededResponse.java new file mode 100644 index 0000000000..c1faabb569 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardEntitiesLimitExceededResponse.java @@ -0,0 +1,50 @@ +/** + * 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.exception; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.springframework.http.HttpStatus; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; + +@Schema +public class ThingsboardEntitiesLimitExceededResponse extends ThingsboardErrorResponse { + + private final EntityType entityType; + + private final Long limit; + + protected ThingsboardEntitiesLimitExceededResponse(String message, EntityType entityType, Long limit) { + super(message, ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED, HttpStatus.FORBIDDEN); + this.entityType = entityType; + this.limit = limit; + } + + public static ThingsboardEntitiesLimitExceededResponse of(final String message, final EntityType entityType, final Long limit) { + return new ThingsboardEntitiesLimitExceededResponse(message, entityType, limit); + } + + @Schema(description = "Entity type", accessMode = Schema.AccessMode.READ_ONLY) + public EntityType getEntityType() { + return entityType; + } + + @Schema(description = "Limit", accessMode = Schema.AccessMode.READ_ONLY) + public Long getLimit() { + return limit; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java index f38b86e394..8a564dedca 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java @@ -15,10 +15,8 @@ */ package org.thingsboard.server.exception; -import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.http.HttpStatus; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @Schema @@ -34,19 +32,9 @@ public class ThingsboardErrorResponse { private final long timestamp; - private EntityType entityType; - - private Long limit; - protected ThingsboardErrorResponse(final String message, final ThingsboardErrorCode errorCode, HttpStatus status) { - this(message, errorCode, null, null, status); - } - - protected ThingsboardErrorResponse(final String message, final ThingsboardErrorCode errorCode, EntityType entityType, Long limit, HttpStatus status) { this.message = message; this.errorCode = errorCode; - this.entityType = entityType; - this.limit = limit; this.status = status; this.timestamp = System.currentTimeMillis(); } @@ -55,14 +43,6 @@ public class ThingsboardErrorResponse { return new ThingsboardErrorResponse(message, errorCode, status); } - public static ThingsboardErrorResponse ofEntityLimitExceeded(final String message, - EntityType entityType, - Long limit, - HttpStatus status) { - return new ThingsboardErrorResponse(message, ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED, - entityType, limit, status); - } - @Schema(description = "HTTP Response Status Code", example = "401", accessMode = Schema.AccessMode.READ_ONLY) public Integer getStatus() { return status.value(); @@ -84,7 +64,8 @@ public class ThingsboardErrorResponse { "\n\n* `32` - Item not found (HTTP: 404 - Not Found)" + "\n\n* `33` - Too many requests (HTTP: 429 - Too Many Requests)" + "\n\n* `34` - Too many updates (Too many updates over Websocket session)" + - "\n\n* `40` - Subscription violation (HTTP: 403 - Forbidden)", + "\n\n* `40` - Subscription violation (HTTP: 403 - Forbidden)" + + "\n\n* `41` - Entities limit exceeded (HTTP: 403 - Forbidden)", example = "10", type = "integer", accessMode = Schema.AccessMode.READ_ONLY) public ThingsboardErrorCode getErrorCode() { @@ -95,12 +76,4 @@ public class ThingsboardErrorResponse { public long getTimestamp() { return timestamp; } - - public EntityType getEntityType() { - return entityType; - } - - public Long getLimit() { - return limit; - } } 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 e6e27e6a75..3ef24e2db9 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -52,7 +52,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.exception.EntitiesLimitException; +import org.thingsboard.server.dao.exception.EntitiesLimitExceededException; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; @@ -147,10 +147,10 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand } else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.DATABASE) { handleDatabaseException(thingsboardException.getCause(), response); } else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED) { - if (thingsboardException.getCause() instanceof EntitiesLimitException entitiesLimitException) { - handleEntitiesLimitException(entitiesLimitException, response); + if (thingsboardException.getCause() instanceof EntitiesLimitExceededException entitiesLimitExceededException) { + handleEntitiesLimitExceededException(entitiesLimitExceededException, response); } else { - handleEntitiesLimitException(thingsboardException, response); + handleEntitiesLimitExceededException(thingsboardException, response); } } else { handleThingsboardException(thingsboardException, response); @@ -227,19 +227,18 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand writeResponse(errorResponse, response); } - private void handleEntitiesLimitException(ThingsboardException entitiesLimitException, HttpServletResponse response) throws IOException { + private void handleEntitiesLimitExceededException(ThingsboardException entitiesLimitExceededException, HttpServletResponse response) throws IOException { response.setStatus(HttpStatus.FORBIDDEN.value()); JacksonUtil.writeValue(response.getWriter(), - JacksonUtil.fromBytes(((HttpClientErrorException) entitiesLimitException.getCause()).getResponseBodyAsByteArray(), Object.class)); + JacksonUtil.fromBytes(((HttpClientErrorException) entitiesLimitExceededException.getCause()).getResponseBodyAsByteArray(), Object.class)); } - private void handleEntitiesLimitException(EntitiesLimitException entitiesLimitException, HttpServletResponse response) throws IOException { - EntityType entityType = entitiesLimitException.getEntityType(); - Long limit = entitiesLimitException.getLimit(); - HttpStatus status = HttpStatus.FORBIDDEN; - response.setStatus(status.value()); + private void handleEntitiesLimitExceededException(EntitiesLimitExceededException entitiesLimitExceededException, HttpServletResponse response) throws IOException { + EntityType entityType = entitiesLimitExceededException.getEntityType(); + Long limit = entitiesLimitExceededException.getLimit(); + response.setStatus(HttpStatus.FORBIDDEN.value()); JacksonUtil.writeValue(response.getWriter(), - ThingsboardErrorResponse.ofEntityLimitExceeded(entitiesLimitException.getMessage(), entityType, limit, status)); + ThingsboardEntitiesLimitExceededResponse.of(entitiesLimitExceededException.getMessage(), entityType, limit)); } private void handleAccessDeniedException(HttpServletResponse response) throws IOException { diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 7fee411704..096c1a814a 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -76,7 +76,7 @@ import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponse; import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; -import org.thingsboard.server.dao.exception.EntitiesLimitException; +import org.thingsboard.server.dao.exception.EntitiesLimitExceededException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; @@ -398,7 +398,7 @@ public class DefaultTransportApiService implements TransportApiService { } catch (JsonProcessingException e) { log.warn("[{}] Failed to lookup device by gateway id and name: [{}]", gatewayId, requestMsg.getDeviceName(), e); throw new RuntimeException(e); - } catch (EntitiesLimitException e) { + } catch (EntitiesLimitExceededException e) { log.warn("[{}][{}] API limit exception: [{}]", e.getTenantId(), gatewayId, e.getMessage()); return TransportApiResponseMsg.newBuilder() .setGetOrCreateDeviceResponseMsg( diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java b/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitExceededException.java similarity index 87% rename from dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java rename to dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitExceededException.java index 559ef94e45..b2907c9104 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java +++ b/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitExceededException.java @@ -19,7 +19,7 @@ import lombok.Getter; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; -public class EntitiesLimitException extends DataValidationException { +public class EntitiesLimitExceededException extends DataValidationException { private static final long serialVersionUID = -9211462514373279196L; @Getter @@ -30,7 +30,7 @@ public class EntitiesLimitException extends DataValidationException { @Getter private final long limit; - public EntitiesLimitException(TenantId tenantId, EntityType entityType, long limit) { + public EntitiesLimitExceededException(TenantId tenantId, EntityType entityType, long limit) { super(entityType.getNormalName() + "s limit reached"); this.tenantId = tenantId; this.entityType = entityType; diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java index 920fd8c144..e305ca556c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -113,7 +113,30 @@ public class DefaultNotifications { .button("${increaseLimitActionLabel}").link("${increaseLimitLink}") .emailTemplate(DefaultEmailTemplate.builder() .subject("${entityType} limit increase request") - .body("${userEmail} has reached the maximum number of ${entityType:lowerCase}s allowed and is requesting an increase to the ${entityType:lowerCase} limit.
${increaseLimitActionLabel}") + .body(""" + + + + + + +
+ + + + + + + + + + + + +
${entityType} limit increase request
${userEmail} has reached the maximum number of ${entityType:lowerCase}s allowed and is requesting an increase to the ${entityType:lowerCase} limit.
+ ${increaseLimitActionLabel} +
+
""") .build()) .build(); public static final DefaultNotification apiFeatureWarningForSysadmin = DefaultNotification.builder() diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index ffa88fe39c..89ab237ed3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.TenantEntityWithDataDao; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.exception.EntitiesLimitException; +import org.thingsboard.server.dao.exception.EntitiesLimitExceededException; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import java.util.HashSet; @@ -124,7 +124,7 @@ public abstract class DataValidator> { EntityType entityType) { if (!apiLimitService.checkEntitiesLimit(tenantId, entityType)) { long limit = apiLimitService.getLimit(tenantId, profileConfiguration -> profileConfiguration.getEntitiesLimit(entityType)); - throw new EntitiesLimitException(tenantId, entityType, limit); + throw new EntitiesLimitExceededException(tenantId, entityType, limit); } } diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 72315e8ed5..2788aa52df 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -53,6 +53,7 @@ export const serverErrorCodesTranslations = new Map([ [Constants.serverErrorCode.itemNotFound, 'server-error.item-not-found'], [Constants.serverErrorCode.tooManyRequests, 'server-error.too-many-requests'], [Constants.serverErrorCode.tooManyUpdates, 'server-error.too-many-updates'], + [Constants.serverErrorCode.entitiesLimitExceeded, 'server-error.entities-limit-exceeded'], ]); export const MediaBreakpoints = { 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 7a7b5bff62..27bd757078 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6140,7 +6140,8 @@ "bad-request-params": "Bad request params", "item-not-found": "Item not found", "too-many-requests": "Too many requests", - "too-many-updates": "Too many updates" + "too-many-updates": "Too many updates", + "entities-limit-exceeded": "Entities limit exceeded" }, "tenant": { "tenant": "Tenant",