From 3c496f474979f586686a8bb6900d2e57fe9cba7a Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 18 Aug 2022 00:43:54 +0300 Subject: [PATCH 001/226] Cassandra cloud connect to Datastax Astra DB using SECURE_BUNDLE, CLIENT_ID and CLIENT_SECRET --- .../install/ThingsboardInstallService.java | 8 ++++ .../install/CassandraKeyspaceService.java | 37 +++++++++++++++++++ .../service/install/NoSqlKeyspaceService.java | 19 ++++++++++ .../src/main/resources/thingsboard.yml | 8 ++++ .../transport/TransportNoSqlTestSuite.java | 1 + .../cassandra/AbstractCassandraCluster.java | 16 ++++++++ .../cassandra/guava/GuavaDriverContext.java | 22 +---------- .../cassandra/guava/GuavaSessionBuilder.java | 14 +------ .../server/dao/util/NoSqlAnyDaoNonCloud.java | 27 ++++++++++++++ .../resources/cassandra/schema-keyspace.cql | 21 +++++++++++ .../resources/cassandra/schema-ts-latest.cql | 6 --- .../main/resources/cassandra/schema-ts.cql | 6 --- .../server/dao/NoSqlDaoServiceTestSuite.java | 1 + .../client/tools/migrator/README.md | 2 +- 14 files changed, 143 insertions(+), 45 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/install/CassandraKeyspaceService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/install/NoSqlKeyspaceService.java create mode 100644 common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlAnyDaoNonCloud.java create mode 100644 dao/src/main/resources/cassandra/schema-keyspace.cql diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 2c3251834f..8eec891b85 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -26,6 +26,7 @@ import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.install.DatabaseEntitiesUpgradeService; import org.thingsboard.server.service.install.DatabaseTsUpgradeService; import org.thingsboard.server.service.install.EntityDatabaseSchemaService; +import org.thingsboard.server.service.install.NoSqlKeyspaceService; import org.thingsboard.server.service.install.SystemDataLoaderService; import org.thingsboard.server.service.install.TsDatabaseSchemaService; import org.thingsboard.server.service.install.TsLatestDatabaseSchemaService; @@ -51,6 +52,9 @@ public class ThingsboardInstallService { @Autowired private EntityDatabaseSchemaService entityDatabaseSchemaService; + @Autowired(required = false) + private NoSqlKeyspaceService noSqlKeyspaceService; + @Autowired private TsDatabaseSchemaService tsDatabaseSchemaService; @@ -248,6 +252,10 @@ public class ThingsboardInstallService { log.info("Installing DataBase schema for timeseries..."); + if (noSqlKeyspaceService != null) { + noSqlKeyspaceService.createDatabaseSchema(); + } + tsDatabaseSchemaService.createDatabaseSchema(); if (tsLatestDatabaseSchemaService != null) { diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraKeyspaceService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraKeyspaceService.java new file mode 100644 index 0000000000..dfdd05e829 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraKeyspaceService.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2022 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.install; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.thingsboard.server.dao.util.NoSqlAnyDaoNonCloud; + +/* +* Create keyspace for Cassandra NoSQL database for non-cloud deployment. +* For cloud service like Astra DBaas admin have to create keyspace manually on cloud UI. +* Then create tokens with database admin role and put it on Thingsboard parameters. +* Without this service cloud DB will end up with exception like +* UnauthorizedException: Missing correct permission on thingsboard +* */ +@Service +@NoSqlAnyDaoNonCloud +@Profile("install") +public class CassandraKeyspaceService extends CassandraAbstractDatabaseSchemaService + implements NoSqlKeyspaceService { + public CassandraKeyspaceService() { + super("schema-keyspace.cql"); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/NoSqlKeyspaceService.java b/application/src/main/java/org/thingsboard/server/service/install/NoSqlKeyspaceService.java new file mode 100644 index 0000000000..5b8d772dca --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/NoSqlKeyspaceService.java @@ -0,0 +1,19 @@ +/** + * Copyright © 2016-2022 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.install; + +public interface NoSqlKeyspaceService extends DatabaseSchemaService { +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 83ff290476..0e8018cebe 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -200,6 +200,14 @@ cassandra: username: "${CASSANDRA_USERNAME:}" # Specify your password password: "${CASSANDRA_PASSWORD:}" + # Astra DB connect https://astra.datastax.com/ + cloud: + # /etc/thingsboard/astra/secure-connect-thingsboard.zip + secure_connect_bundle_path: "${CASSANDRA_CLOUD_SECURE_BUNDLE_PATH:}" + # DucitQPHMzPCBOZqFYexAfKk + client_id: "${CASSANDRA_CLOUD_CLIENT_ID:}" + # ZnF7FpuHp43FP5BzM+KY8wGmSb4Ql6BhT4Z7sOU13ze+gXQ-n7OkFpNuB,oACUIQObQnK0g4bSPoZhK5ejkcF9F.j6f64j71Sr.tiRe0Fsq2hPS1ZCGSfAaIgg63IydG + client_secret: "${CASSANDRA_CLOUD_CLIENT_SECRET:}" # Cassandra cluster connection socket parameters # socket: diff --git a/application/src/test/java/org/thingsboard/server/transport/TransportNoSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/transport/TransportNoSqlTestSuite.java index 255057cc2f..caaf5be6de 100644 --- a/application/src/test/java/org/thingsboard/server/transport/TransportNoSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/transport/TransportNoSqlTestSuite.java @@ -35,6 +35,7 @@ public class TransportNoSqlTestSuite { public static CustomCassandraCQLUnit cassandraUnit = new CustomCassandraCQLUnit( Arrays.asList( + new ClassPathCQLDataSet("cassandra/schema-keyspace.cql", false, false), new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false), new ClassPathCQLDataSet("cassandra/schema-ts-latest.cql", false, false) ), diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java index 6586f42d23..f3f76aba9f 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java @@ -24,11 +24,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; import org.springframework.core.env.Profiles; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.dao.cassandra.guava.GuavaSession; import org.thingsboard.server.dao.cassandra.guava.GuavaSessionBuilder; import org.thingsboard.server.dao.cassandra.guava.GuavaSessionUtils; import javax.annotation.PreDestroy; +import java.nio.file.Paths; @Slf4j public abstract class AbstractCassandraCluster { @@ -40,6 +42,13 @@ public abstract class AbstractCassandraCluster { @Value("${cassandra.local_datacenter:datacenter1}") private String localDatacenter; + @Value("${cassandra.cloud.secure_connect_bundle_path:}") + private String cloudSecureConnectBundlePath; + @Value("${cassandra.cloud.client_id:}") + private String cloudClientId; + @Value("${cassandra.cloud.client_secret:}") + private String cloudClientSecret; + @Autowired private CassandraDriverOptions driverOptions; @@ -86,7 +95,14 @@ public abstract class AbstractCassandraCluster { this.sessionBuilder.withKeyspace(this.keyspaceName); } this.sessionBuilder.withLocalDatacenter(localDatacenter); + + if (StringUtils.isNotBlank(cloudSecureConnectBundlePath)) { + this.sessionBuilder.withCloudSecureConnectBundle(Paths.get(cloudSecureConnectBundlePath)); + this.sessionBuilder.withAuthCredentials(cloudClientId, cloudClientSecret); + } + session = sessionBuilder.build(); + if (this.metrics && this.jmx) { MetricRegistry registry = session.getMetrics().orElseThrow( diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaDriverContext.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaDriverContext.java index b5472d78a2..f193b0a7fe 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaDriverContext.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaDriverContext.java @@ -40,26 +40,8 @@ import java.util.function.Predicate; */ public class GuavaDriverContext extends DefaultDriverContext { - public GuavaDriverContext( - DriverConfigLoader configLoader, - List> typeCodecs, - NodeStateListener nodeStateListener, - SchemaChangeListener schemaChangeListener, - RequestTracker requestTracker, - Map localDatacenters, - Map> nodeFilters, - ClassLoader classLoader) { - super( - configLoader, - ProgrammaticArguments.builder() - .addTypeCodecs(typeCodecs.toArray(new TypeCodec[0])) - .withNodeStateListener(nodeStateListener) - .withSchemaChangeListener(schemaChangeListener) - .withRequestTracker(requestTracker) - .withLocalDatacenters(localDatacenters) - .withNodeFilters(nodeFilters) - .withClassLoader(classLoader) - .build()); + public GuavaDriverContext(DriverConfigLoader configLoader, ProgrammaticArguments programmaticArguments) { + super(configLoader, programmaticArguments); } @Override diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSessionBuilder.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSessionBuilder.java index c95c653d64..60ff71a8bd 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSessionBuilder.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSessionBuilder.java @@ -25,18 +25,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; public class GuavaSessionBuilder extends SessionBuilder { @Override - protected DriverContext buildContext( - DriverConfigLoader configLoader, - ProgrammaticArguments programmaticArguments) { - return new GuavaDriverContext( - configLoader, - programmaticArguments.getTypeCodecs(), - programmaticArguments.getNodeStateListener(), - programmaticArguments.getSchemaChangeListener(), - programmaticArguments.getRequestTracker(), - programmaticArguments.getLocalDatacenters(), - programmaticArguments.getNodeFilters(), - programmaticArguments.getClassLoader()); + protected DriverContext buildContext(DriverConfigLoader configLoader, ProgrammaticArguments programmaticArguments) { + return new GuavaDriverContext(configLoader, programmaticArguments); } @Override diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlAnyDaoNonCloud.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlAnyDaoNonCloud.java new file mode 100644 index 0000000000..0ec01c8b0a --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlAnyDaoNonCloud.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2022 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.dao.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnExpression("('${database.ts.type}'=='cassandra' || '${database.ts_latest.type}'=='cassandra') " + + "&& ('${cassandra.cloud.secure_connect_bundle_path}' == null || '${cassandra.cloud.secure_connect_bundle_path}'.isBlank() )") +public @interface NoSqlAnyDaoNonCloud { +} diff --git a/dao/src/main/resources/cassandra/schema-keyspace.cql b/dao/src/main/resources/cassandra/schema-keyspace.cql new file mode 100644 index 0000000000..8323f149ce --- /dev/null +++ b/dao/src/main/resources/cassandra/schema-keyspace.cql @@ -0,0 +1,21 @@ +-- +-- Copyright © 2016-2022 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. +-- + +CREATE KEYSPACE IF NOT EXISTS thingsboard +WITH replication = { + 'class' : 'SimpleStrategy', + 'replication_factor' : 1 +}; diff --git a/dao/src/main/resources/cassandra/schema-ts-latest.cql b/dao/src/main/resources/cassandra/schema-ts-latest.cql index 0de4e88158..10b3edb84e 100644 --- a/dao/src/main/resources/cassandra/schema-ts-latest.cql +++ b/dao/src/main/resources/cassandra/schema-ts-latest.cql @@ -14,12 +14,6 @@ -- limitations under the License. -- -CREATE KEYSPACE IF NOT EXISTS thingsboard -WITH replication = { - 'class' : 'SimpleStrategy', - 'replication_factor' : 1 -}; - CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_latest_cf ( entity_type text, // (DEVICE, CUSTOMER, TENANT) entity_id timeuuid, diff --git a/dao/src/main/resources/cassandra/schema-ts.cql b/dao/src/main/resources/cassandra/schema-ts.cql index 9cb609add2..6ab099f841 100644 --- a/dao/src/main/resources/cassandra/schema-ts.cql +++ b/dao/src/main/resources/cassandra/schema-ts.cql @@ -14,12 +14,6 @@ -- limitations under the License. -- -CREATE KEYSPACE IF NOT EXISTS thingsboard -WITH replication = { - 'class' : 'SimpleStrategy', - 'replication_factor' : 1 -}; - CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_cf ( entity_type text, // (DEVICE, CUSTOMER, TENANT) entity_id timeuuid, diff --git a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java index 838be51349..e601218845 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java @@ -33,6 +33,7 @@ public class NoSqlDaoServiceTestSuite { public static CustomCassandraCQLUnit cassandraUnit = new CustomCassandraCQLUnit( Arrays.asList( + new ClassPathCQLDataSet("cassandra/schema-keyspace.cql", false, false), new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false), new ClassPathCQLDataSet("cassandra/schema-ts-latest.cql", false, false) ), diff --git a/tools/src/main/java/org/thingsboard/client/tools/migrator/README.md b/tools/src/main/java/org/thingsboard/client/tools/migrator/README.md index a468cbd3af..6995632514 100644 --- a/tools/src/main/java/org/thingsboard/client/tools/migrator/README.md +++ b/tools/src/main/java/org/thingsboard/client/tools/migrator/README.md @@ -63,7 +63,7 @@ Tool execution time depends on DB size, CPU resources and Disk throughput * Note that this this part works only for single node Cassandra Cluster. If you have more nodes - it is better to use `sstableloader` tool. 1. [Optional] install Cassandra on the instance -2. [Optional] Using `cqlsh` create `thingsboard` keyspace and requred tables from this files `schema-ts.cql` and `schema-ts-latest.cql` using `source` command +2. [Optional] Using `cqlsh` create `thingsboard` keyspace and requred tables from this files `schema-keyspace.cql`, `schema-ts.cql` and `schema-ts-latest.cql` using `source` command 3. Stop Cassandra 4. Look at `/var/lib/cassandra/data/thingsboard` and check for names of data folders 5. Copy generated SSTable files into cassandra data dir using next command: From dc07e9ae459d6cb7f0ec99ba1664df0180d14463 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 16 Sep 2022 18:57:50 +0300 Subject: [PATCH 002/226] jwt settings --- .../server/config/JwtSettings.java | 44 +++++++++++++++++++ .../src/main/resources/thingsboard.yml | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java index 95e510612a..1fc0b4bd8c 100644 --- a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java +++ b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java @@ -15,15 +15,30 @@ */ package org.thingsboard.server.config; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.model.JwtToken; +import org.thingsboard.server.dao.settings.AdminSettingsService; + +import javax.annotation.PostConstruct; +import java.nio.charset.StandardCharsets; +import java.util.Base64; @Component @ConfigurationProperties(prefix = "security.jwt") @Data +@Slf4j public class JwtSettings { + static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; + static final String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; /** * {@link JwtToken} will expire after this time. */ @@ -36,6 +51,7 @@ public class JwtSettings { /** * Key is used to sign {@link JwtToken}. + * Base64 encoded */ private String tokenSigningKey; @@ -44,4 +60,32 @@ public class JwtSettings { */ private Integer refreshTokenExpTime; + @JsonIgnore + @Autowired + private AdminSettingsService adminSettingsService; + + @PostConstruct + public void init() { + AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); + if (adminJwtSettings == null) { + if (TOKEN_SIGNING_KEY_DEFAULT.equals(tokenSigningKey)) { + log.warn("JWT token signing key is default. Generating a new random key"); + tokenSigningKey = Base64.getEncoder().encodeToString(RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)); + } + adminJwtSettings = new AdminSettings(); + adminJwtSettings.setTenantId(TenantId.SYS_TENANT_ID); + adminJwtSettings.setKey(ADMIN_SETTINGS_JWT_KEY); + adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(this)); + log.info("Saving new JWT admin settings. From this moment, the JWT parameters from YAML and ENV will be ignored"); + adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminJwtSettings); + } else { + log.debug("Loading the JWT admin settings"); + JwtSettings jwtSettings = JacksonUtil.treeToValue(adminJwtSettings.getJsonValue(), JwtSettings.class); + this.setRefreshTokenExpTime(jwtSettings.getRefreshTokenExpTime()); + this.setTokenExpirationTime(jwtSettings.getTokenExpirationTime()); + this.setTokenIssuer(jwtSettings.getTokenIssuer()); + this.setTokenSigningKey(jwtSettings.getTokenSigningKey()); + } + } + } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 17b996934e..a6e8637247 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -111,7 +111,7 @@ security: tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}" # Number of seconds (2.5 hours) refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week) tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}" - tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" + tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" # Base64 encoded # Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator user_token_access_enabled: "${SECURITY_USER_TOKEN_ACCESS_ENABLED:true}" # Enable/disable case-sensitive username login From 9b519d33a19532f4cca8973e521d5ee1186d5b35 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 19 Sep 2022 12:43:43 +0300 Subject: [PATCH 003/226] jwt settings install and upgrade --- .../server/config/JwtSettings.java | 28 ++++----- .../install/ThingsboardInstallService.java | 12 ++++ .../ConditionValidatorUpgradeService.java | 22 +++++++ .../ConditionValidatorUpgradeServiceImpl.java | 63 +++++++++++++++++++ .../DefaultSystemDataLoaderService.java | 33 ++++++++++ .../install/SystemDataLoaderService.java | 2 + .../update/DefaultDataUpdateService.java | 4 ++ .../src/main/resources/thingsboard.yml | 2 +- 8 files changed, 148 insertions(+), 18 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java diff --git a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java index 1fc0b4bd8c..14e228bde8 100644 --- a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java +++ b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java @@ -18,7 +18,6 @@ package org.thingsboard.server.config; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @@ -29,15 +28,13 @@ import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.dao.settings.AdminSettingsService; import javax.annotation.PostConstruct; -import java.nio.charset.StandardCharsets; -import java.util.Base64; @Component @ConfigurationProperties(prefix = "security.jwt") @Data @Slf4j public class JwtSettings { - static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; + public static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; static final String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; /** * {@link JwtToken} will expire after this time. @@ -67,25 +64,22 @@ public class JwtSettings { @PostConstruct public void init() { AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); - if (adminJwtSettings == null) { - if (TOKEN_SIGNING_KEY_DEFAULT.equals(tokenSigningKey)) { - log.warn("JWT token signing key is default. Generating a new random key"); - tokenSigningKey = Base64.getEncoder().encodeToString(RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)); - } - adminJwtSettings = new AdminSettings(); - adminJwtSettings.setTenantId(TenantId.SYS_TENANT_ID); - adminJwtSettings.setKey(ADMIN_SETTINGS_JWT_KEY); - adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(this)); - log.info("Saving new JWT admin settings. From this moment, the JWT parameters from YAML and ENV will be ignored"); - adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminJwtSettings); - } else { - log.debug("Loading the JWT admin settings"); + if (adminJwtSettings != null) { + log.debug("Loading the JWT admin settings from database"); JwtSettings jwtSettings = JacksonUtil.treeToValue(adminJwtSettings.getJsonValue(), JwtSettings.class); this.setRefreshTokenExpTime(jwtSettings.getRefreshTokenExpTime()); this.setTokenExpirationTime(jwtSettings.getTokenExpirationTime()); this.setTokenIssuer(jwtSettings.getTokenIssuer()); this.setTokenSigningKey(jwtSettings.getTokenSigningKey()); } + + if (hasDefaultTokenSigningKey()) { + log.warn("JWT token signing key is default. This is a security issue. Please, consider to set unique value"); + } + } + + public boolean hasDefaultTokenSigningKey() { + return TOKEN_SIGNING_KEY_DEFAULT.equals(tokenSigningKey); } } diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 40e15c7b0e..c83552e777 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -33,6 +33,7 @@ import org.thingsboard.server.service.install.migrate.EntitiesMigrateService; import org.thingsboard.server.service.install.migrate.TsLatestMigrateService; import org.thingsboard.server.service.install.update.CacheCleanupService; import org.thingsboard.server.service.install.update.DataUpdateService; +import org.thingsboard.server.service.install.ConditionValidatorUpgradeService; @Service @Profile("install") @@ -84,11 +85,17 @@ public class ThingsboardInstallService { @Autowired(required = false) private TsLatestMigrateService latestMigrateService; + @Autowired + private ConditionValidatorUpgradeService conditionValidatorUpgradeService; + + public void performInstall() { try { if (isUpgrade) { log.info("Starting ThingsBoard Upgrade from version {} ...", upgradeFromVersion); + conditionValidatorUpgradeService.validateConditionsBeforeUpgrade(upgradeFromVersion); + cacheCleanupService.clearCache(upgradeFromVersion); if ("2.5.0-cassandra".equals(upgradeFromVersion)) { @@ -224,6 +231,10 @@ public class ThingsboardInstallService { log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); break; + case "3.4.0": + log.info("Upgrading ThingsBoard from version 3.4.0 to 3.5.0 ..."); + dataUpdateService.updateData("3.4.0"); + break; //TODO update CacheCleanupService on the next version upgrade @@ -257,6 +268,7 @@ public class ThingsboardInstallService { systemDataLoaderService.createSysAdmin(); systemDataLoaderService.createDefaultTenantProfiles(); systemDataLoaderService.createAdminSettings(); + systemDataLoaderService.createJwtAdminSettings(); systemDataLoaderService.loadSystemWidgets(); systemDataLoaderService.createOAuth2Templates(); systemDataLoaderService.createQueues(); diff --git a/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeService.java new file mode 100644 index 0000000000..ec98a2f37e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeService.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2022 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.install; + +public interface ConditionValidatorUpgradeService { + + void validateConditionsBeforeUpgrade(String fromVersion) throws Exception; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java new file mode 100644 index 0000000000..f70086abee --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2022 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.install; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.config.JwtSettings; +import org.thingsboard.server.dao.settings.AdminSettingsService; + +import javax.validation.ValidationException; + +import static org.thingsboard.server.config.JwtSettings.ADMIN_SETTINGS_JWT_KEY; + +@Service +@Profile("install") +@RequiredArgsConstructor +@Slf4j +public class ConditionValidatorUpgradeServiceImpl implements ConditionValidatorUpgradeService { + + private final AdminSettingsService adminSettingsService; + + private final JwtSettings jwtSettings; + + @Override + public void validateConditionsBeforeUpgrade(String fromVersion) throws Exception { + log.info("Validating conditions before upgrade.."); + validateJwtTokenSigningKey(); + } + + void validateJwtTokenSigningKey() { + AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); + if (adminJwtSettings == null) { + if (jwtSettings.hasDefaultTokenSigningKey()) { + String allowDefaultJwtSigningKey = System.getenv("TB_ALLOW_DEFAULT_JWT_SIGNING_KEY"); + if ("true".equalsIgnoreCase(allowDefaultJwtSigningKey)) { + log.warn("Default JWT signing key is allowed. This is a security issue. Please, consider to set a strong key in admin settings"); + } else { + String message = "Please, set a unique signing key with env variable JWT_TOKEN_SIGNING_KEY. Key is a Base64 encoded phrase. This will require to generate new tokens for all users and API that uses JWT tokens. To allow insecure JWS use TB_ALLOW_DEFAULT_JWT_SIGNING_KEY=true"; + log.error(message); + throw new ValidationException(message); + } + } + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 78e03ffe1a..a113f1e6d1 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -22,6 +22,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -29,6 +30,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Profile; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.Customer; @@ -82,6 +84,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.config.JwtSettings; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceCredentialsService; @@ -100,13 +103,18 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.TreeMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import static org.thingsboard.server.config.JwtSettings.ADMIN_SETTINGS_JWT_KEY; + @Service @Profile("install") @Slf4j @@ -167,6 +175,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Autowired private QueueService queueService; + @Autowired + private JwtSettings jwtSettings; + @Bean protected BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -656,4 +667,26 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { queueService.saveQueue(sequentialByOriginatorQueue); } } + + @Override + public void createJwtAdminSettings() throws Exception { + Objects.requireNonNull(jwtSettings,"JWT settings is null"); + AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); + if (adminJwtSettings == null) { + if (jwtSettings.hasDefaultTokenSigningKey()) { + String allowDefaultJwtSigningKey = System.getenv("TB_ALLOW_DEFAULT_JWT_SIGNING_KEY"); + if (!"true".equalsIgnoreCase(allowDefaultJwtSigningKey)) { + log.warn("JWT token signing key is default. Generating a new random key"); + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); + } + } + adminJwtSettings = new AdminSettings(); + adminJwtSettings.setTenantId(TenantId.SYS_TENANT_ID); + adminJwtSettings.setKey(ADMIN_SETTINGS_JWT_KEY); + adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(jwtSettings)); + log.info("Saving new JWT admin settings. From this moment, the JWT parameters from YAML and ENV will be ignored"); + adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminJwtSettings); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java index 1ceb1be289..4ef8241a3b 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java @@ -23,6 +23,8 @@ public interface SystemDataLoaderService { void createAdminSettings() throws Exception; + void createJwtAdminSettings() throws Exception; + void createOAuth2Templates() throws Exception; void loadSystemWidgets() throws Exception; diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index b90da586fc..862f0a92b8 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -159,6 +159,10 @@ public class DefaultDataUpdateService implements DataUpdateService { tenantsProfileQueueConfigurationUpdater.updateEntities(); rateLimitsUpdater.updateEntities(); break; + case "3.4.0": + log.info("Updating data from version 3.4.0 to 3.5.0 ..."); + systemDataLoaderService.createJwtAdminSettings(); + break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index a6e8637247..0e6a005e4d 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -107,7 +107,7 @@ plugins: # Security parameters security: # JWT Token parameters - jwt: + jwt: # Since 3.5.0 values are persisted to the database during install or upgrade. On Install, the key will be generated randomly if no custom value set. tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}" # Number of seconds (2.5 hours) refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week) tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}" From 7c8db6cac7245abcd42905c91864e2ece2d57c0b Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 19 Sep 2022 15:44:28 +0300 Subject: [PATCH 004/226] jwt settings service implementation --- .../server/config/JwtSettings.java | 38 +----- .../server/config/JwtSettingsService.java | 117 ++++++++++++++++++ .../ConditionValidatorUpgradeServiceImpl.java | 31 +---- .../DefaultSystemDataLoaderService.java | 37 ++---- 4 files changed, 128 insertions(+), 95 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/config/JwtSettingsService.java diff --git a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java index 14e228bde8..b85f1738c9 100644 --- a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java +++ b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java @@ -15,27 +15,16 @@ */ package org.thingsboard.server.config; -import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.AdminSettings; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.dao.settings.AdminSettingsService; - -import javax.annotation.PostConstruct; @Component @ConfigurationProperties(prefix = "security.jwt") @Data -@Slf4j public class JwtSettings { - public static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; - static final String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; + /** * {@link JwtToken} will expire after this time. */ @@ -57,29 +46,4 @@ public class JwtSettings { */ private Integer refreshTokenExpTime; - @JsonIgnore - @Autowired - private AdminSettingsService adminSettingsService; - - @PostConstruct - public void init() { - AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); - if (adminJwtSettings != null) { - log.debug("Loading the JWT admin settings from database"); - JwtSettings jwtSettings = JacksonUtil.treeToValue(adminJwtSettings.getJsonValue(), JwtSettings.class); - this.setRefreshTokenExpTime(jwtSettings.getRefreshTokenExpTime()); - this.setTokenExpirationTime(jwtSettings.getTokenExpirationTime()); - this.setTokenIssuer(jwtSettings.getTokenIssuer()); - this.setTokenSigningKey(jwtSettings.getTokenSigningKey()); - } - - if (hasDefaultTokenSigningKey()) { - log.warn("JWT token signing key is default. This is a security issue. Please, consider to set unique value"); - } - } - - public boolean hasDefaultTokenSigningKey() { - return TOKEN_SIGNING_KEY_DEFAULT.equals(tokenSigningKey); - } - } diff --git a/application/src/main/java/org/thingsboard/server/config/JwtSettingsService.java b/application/src/main/java/org/thingsboard/server/config/JwtSettingsService.java new file mode 100644 index 0000000000..bb7d3abc55 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/JwtSettingsService.java @@ -0,0 +1,117 @@ +/** + * Copyright © 2016-2022 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.config; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.settings.AdminSettingsService; + +import javax.annotation.PostConstruct; +import javax.validation.ValidationException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +@Slf4j +public class JwtSettingsService { + + static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; + static final String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; + static final String TB_ALLOW_DEFAULT_JWT_SIGNING_KEY = "TB_ALLOW_DEFAULT_JWT_SIGNING_KEY"; + + private final AdminSettingsService adminSettingsService; + + @Getter + private final JwtSettings jwtSettings; + + @PostConstruct + public void init() { + AdminSettings adminJwtSettings = findJwtAdminSettings(); + if (adminJwtSettings != null) { + log.debug("Loading the JWT admin settings from database"); + JwtSettings jwtLoaded = JacksonUtil.treeToValue(adminJwtSettings.getJsonValue(), JwtSettings.class); + jwtSettings.setRefreshTokenExpTime(jwtLoaded.getRefreshTokenExpTime()); + jwtSettings.setTokenExpirationTime(jwtLoaded.getTokenExpirationTime()); + jwtSettings.setTokenIssuer(jwtLoaded.getTokenIssuer()); + jwtSettings.setTokenSigningKey(jwtLoaded.getTokenSigningKey()); + } + + if (hasDefaultTokenSigningKey()) { + log.warn("JWT token signing key is default. This is a security issue. Please, consider to set unique value"); + } + } + + public boolean hasDefaultTokenSigningKey() { + return TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey()); + } + + public void createJwtAdminSettings() { + Objects.requireNonNull(jwtSettings, "JWT settings is null"); + if (!isJwtAdminSettingsExists()) { + if (hasDefaultTokenSigningKey()) { + if (!isAllowedDefaultJwtSigningKey()) { + log.warn("JWT token signing key is default. Generating a new random key"); + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); + } + } + AdminSettings adminJwtSettings = new AdminSettings(); + adminJwtSettings.setTenantId(TenantId.SYS_TENANT_ID); + adminJwtSettings.setKey(ADMIN_SETTINGS_JWT_KEY); + adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(jwtSettings)); + log.info("Saving new JWT admin settings. From this moment, the JWT parameters from YAML and ENV will be ignored"); + adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminJwtSettings); + } + } + + public boolean isJwtAdminSettingsExists() { + return findJwtAdminSettings() == null; + } + + AdminSettings findJwtAdminSettings() { + return adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); + } + + /* + * Allowing default JWT signing key is not secure + * */ + public boolean isAllowedDefaultJwtSigningKey() { + String allowDefaultJwtSigningKey = System.getenv(TB_ALLOW_DEFAULT_JWT_SIGNING_KEY); + return "true".equalsIgnoreCase(allowDefaultJwtSigningKey); + } + + public void validateJwtTokenSigningKey() { + if (!isJwtAdminSettingsExists()) { + if (hasDefaultTokenSigningKey()) { + if (isAllowedDefaultJwtSigningKey()) { + log.warn("Default JWT signing key is allowed. This is a security issue. Please, consider to set a strong key in admin settings"); + } else { + String message = "Please, set a unique signing key with env variable JWT_TOKEN_SIGNING_KEY. Key is a Base64 encoded phrase. This will require to generate new tokens for all users and API that uses JWT tokens. To allow insecure JWS use TB_ALLOW_DEFAULT_JWT_SIGNING_KEY=true"; + log.error(message); + throw new ValidationException(message); + } + } + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java index f70086abee..27117239f0 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java @@ -19,14 +19,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.AdminSettings; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.config.JwtSettings; -import org.thingsboard.server.dao.settings.AdminSettingsService; - -import javax.validation.ValidationException; - -import static org.thingsboard.server.config.JwtSettings.ADMIN_SETTINGS_JWT_KEY; +import org.thingsboard.server.config.JwtSettingsService; @Service @Profile("install") @@ -34,30 +27,12 @@ import static org.thingsboard.server.config.JwtSettings.ADMIN_SETTINGS_JWT_KEY; @Slf4j public class ConditionValidatorUpgradeServiceImpl implements ConditionValidatorUpgradeService { - private final AdminSettingsService adminSettingsService; - - private final JwtSettings jwtSettings; + private final JwtSettingsService jwtSettingsService; @Override public void validateConditionsBeforeUpgrade(String fromVersion) throws Exception { log.info("Validating conditions before upgrade.."); - validateJwtTokenSigningKey(); - } - - void validateJwtTokenSigningKey() { - AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); - if (adminJwtSettings == null) { - if (jwtSettings.hasDefaultTokenSigningKey()) { - String allowDefaultJwtSigningKey = System.getenv("TB_ALLOW_DEFAULT_JWT_SIGNING_KEY"); - if ("true".equalsIgnoreCase(allowDefaultJwtSigningKey)) { - log.warn("Default JWT signing key is allowed. This is a security issue. Please, consider to set a strong key in admin settings"); - } else { - String message = "Please, set a unique signing key with env variable JWT_TOKEN_SIGNING_KEY. Key is a Base64 encoded phrase. This will require to generate new tokens for all users and API that uses JWT tokens. To allow insecure JWS use TB_ALLOW_DEFAULT_JWT_SIGNING_KEY=true"; - log.error(message); - throw new ValidationException(message); - } - } - } + jwtSettingsService.validateJwtTokenSigningKey(); } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index a113f1e6d1..9c9ffb5188 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -22,7 +22,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -30,7 +29,6 @@ import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Profile; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.Customer; @@ -84,7 +82,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.config.JwtSettings; +import org.thingsboard.server.config.JwtSettingsService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceCredentialsService; @@ -103,18 +101,13 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.nio.charset.StandardCharsets; import java.util.Arrays; -import java.util.Base64; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.TreeMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import static org.thingsboard.server.config.JwtSettings.ADMIN_SETTINGS_JWT_KEY; - @Service @Profile("install") @Slf4j @@ -176,7 +169,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { private QueueService queueService; @Autowired - private JwtSettings jwtSettings; + private JwtSettingsService jwtSettingsService; @Bean protected BCryptPasswordEncoder passwordEncoder() { @@ -274,6 +267,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings); } + @Override + public void createJwtAdminSettings() throws Exception { + jwtSettingsService.createJwtAdminSettings(); + } + @Override public void createOAuth2Templates() throws Exception { installScripts.createOAuth2Templates(); @@ -668,25 +666,4 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { } } - @Override - public void createJwtAdminSettings() throws Exception { - Objects.requireNonNull(jwtSettings,"JWT settings is null"); - AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); - if (adminJwtSettings == null) { - if (jwtSettings.hasDefaultTokenSigningKey()) { - String allowDefaultJwtSigningKey = System.getenv("TB_ALLOW_DEFAULT_JWT_SIGNING_KEY"); - if (!"true".equalsIgnoreCase(allowDefaultJwtSigningKey)) { - log.warn("JWT token signing key is default. Generating a new random key"); - jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); - } - } - adminJwtSettings = new AdminSettings(); - adminJwtSettings.setTenantId(TenantId.SYS_TENANT_ID); - adminJwtSettings.setKey(ADMIN_SETTINGS_JWT_KEY); - adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(jwtSettings)); - log.info("Saving new JWT admin settings. From this moment, the JWT parameters from YAML and ENV will be ignored"); - adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminJwtSettings); - } - } - } From 5ea3c9ff6a16d83c4f98e092670a23fdd0583a2b Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 19 Sep 2022 17:23:27 +0300 Subject: [PATCH 005/226] jwt settings service instead jwt settings data object --- .../server/config/JwtSettings.java | 1 - .../install/ThingsboardInstallService.java | 1 - .../security/auth/TokenOutdatingService.java | 6 ++--- .../security/model/token/JwtTokenFactory.java | 22 ++++++++----------- .../security/auth/JwtTokenFactoryTest.java | 8 ++++++- .../security/auth/TokenOutdatingTest.java | 10 +++++++-- 6 files changed, 27 insertions(+), 21 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java index b85f1738c9..e5667dc811 100644 --- a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java +++ b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java @@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.security.model.JwtToken; @ConfigurationProperties(prefix = "security.jwt") @Data public class JwtSettings { - /** * {@link JwtToken} will expire after this time. */ diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index c83552e777..e8972696da 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -88,7 +88,6 @@ public class ThingsboardInstallService { @Autowired private ConditionValidatorUpgradeService conditionValidatorUpgradeService; - public void performInstall() { try { if (isUpgrade) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java index a623fc6862..73bb97ea0b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.config.JwtSettings; +import org.thingsboard.server.config.JwtSettingsService; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import javax.annotation.PostConstruct; @@ -39,7 +39,7 @@ import static java.util.concurrent.TimeUnit.SECONDS; public class TokenOutdatingService { private final CacheManager cacheManager; private final JwtTokenFactory tokenFactory; - private final JwtSettings jwtSettings; + private final JwtSettingsService jwtSettingsService; private Cache usersUpdateTimeCache; @PostConstruct @@ -58,7 +58,7 @@ public class TokenOutdatingService { return Optional.ofNullable(usersUpdateTimeCache.get(toKey(userId), Long.class)) .map(outdatageTime -> { - if (System.currentTimeMillis() - outdatageTime <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime())) { + if (System.currentTimeMillis() - outdatageTime <= SECONDS.toMillis(jwtSettingsService.getJwtSettings().getRefreshTokenExpTime())) { return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); } else { /* diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index a8e6f9cb5c..a2fe509a1a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -24,9 +24,9 @@ import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.UnsupportedJwtException; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.config.JwtSettings; +import org.thingsboard.server.config.JwtSettingsService; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; @@ -49,6 +49,7 @@ import java.util.UUID; import java.util.stream.Collectors; @Component +@RequiredArgsConstructor @Slf4j public class JwtTokenFactory { @@ -61,12 +62,7 @@ public class JwtTokenFactory { private static final String TENANT_ID = "tenantId"; private static final String CUSTOMER_ID = "customerId"; - private final JwtSettings settings; - - @Autowired - public JwtTokenFactory(JwtSettings settings) { - this.settings = settings; - } + private final JwtSettingsService jwtSettingsService; /** * Factory method for issuing new JWT Tokens. @@ -79,7 +75,7 @@ public class JwtTokenFactory { UserPrincipal principal = securityUser.getUserPrincipal(); JwtBuilder jwtBuilder = setUpToken(securityUser, securityUser.getAuthorities().stream() - .map(GrantedAuthority::getAuthority).collect(Collectors.toList()), settings.getTokenExpirationTime()); + .map(GrantedAuthority::getAuthority).collect(Collectors.toList()), jwtSettingsService.getJwtSettings().getTokenExpirationTime()); jwtBuilder.claim(FIRST_NAME, securityUser.getFirstName()) .claim(LAST_NAME, securityUser.getLastName()) .claim(ENABLED, securityUser.isEnabled()) @@ -138,7 +134,7 @@ public class JwtTokenFactory { public JwtToken createRefreshToken(SecurityUser securityUser) { UserPrincipal principal = securityUser.getUserPrincipal(); - String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), settings.getRefreshTokenExpTime()) + String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), jwtSettingsService.getJwtSettings().getRefreshTokenExpTime()) .claim(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID) .setId(UUID.randomUUID().toString()).compact(); @@ -188,16 +184,16 @@ public class JwtTokenFactory { return Jwts.builder() .setClaims(claims) - .setIssuer(settings.getTokenIssuer()) + .setIssuer(jwtSettingsService.getJwtSettings().getTokenIssuer()) .setIssuedAt(Date.from(currentTime.toInstant())) .setExpiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant())) - .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey()); + .signWith(SignatureAlgorithm.HS512, jwtSettingsService.getJwtSettings().getTokenSigningKey()); } public Jws parseTokenClaims(JwtToken token) { try { return Jwts.parser() - .setSigningKey(settings.getTokenSigningKey()) + .setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey()) .parseClaimsJws(token.getToken()); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { log.debug("Invalid JWT Token", ex); diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index f865c9b5e0..c3223b920e 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.config.JwtSettings; +import org.thingsboard.server.config.JwtSettingsService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.AccessJwtToken; @@ -36,6 +37,8 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; public class JwtTokenFactoryTest { @@ -50,7 +53,10 @@ public class JwtTokenFactoryTest { jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2)); jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7)); - tokenFactory = new JwtTokenFactory(jwtSettings); + JwtSettingsService jwtSettingsService = mock(JwtSettingsService.class); + willReturn(jwtSettings).given(jwtSettingsService).getJwtSettings(); + + tokenFactory = new JwtTokenFactory(jwtSettingsService); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java index f804d4dcfd..ea5c7f20e8 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.config.JwtSettings; +import org.thingsboard.server.config.JwtSettingsService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; @@ -50,6 +51,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -71,10 +73,14 @@ public class TokenOutdatingTest { jwtSettings.setTokenExpirationTime((int) MINUTES.toSeconds(10)); jwtSettings.setRefreshTokenExpTime((int) DAYS.toSeconds(7)); jwtSettings.setTokenSigningKey("secret"); - tokenFactory = new JwtTokenFactory(jwtSettings); + + JwtSettingsService jwtSettingsService = mock(JwtSettingsService.class); + willReturn(jwtSettings).given(jwtSettingsService).getJwtSettings(); + + tokenFactory = new JwtTokenFactory(jwtSettingsService); cacheManager = new ConcurrentMapCacheManager(); - tokenOutdatingService = new TokenOutdatingService(cacheManager, tokenFactory, jwtSettings); + tokenOutdatingService = new TokenOutdatingService(cacheManager, tokenFactory, jwtSettingsService); tokenOutdatingService.initCache(); userId = new UserId(UUID.randomUUID()); From c313e1cf9cfd2fae66346c8137f64b1179b679e7 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 19 Sep 2022 19:09:24 +0300 Subject: [PATCH 006/226] jwt settings - running install on msa black box tests --- .../server/ThingsboardInstallApplication.java | 1 + .../server/config/{ => jwt}/JwtSettings.java | 2 +- .../config/{ => jwt}/JwtSettingsService.java | 33 +++++++++++-------- .../ConditionValidatorUpgradeServiceImpl.java | 4 +-- .../DefaultSystemDataLoaderService.java | 2 +- .../security/auth/TokenOutdatingService.java | 2 +- .../security/model/token/JwtTokenFactory.java | 2 +- .../security/auth/JwtTokenFactoryTest.java | 4 +-- .../security/auth/TokenOutdatingTest.java | 4 +-- packaging/java/scripts/install/logback.xml | 4 +++ 10 files changed, 34 insertions(+), 24 deletions(-) rename application/src/main/java/org/thingsboard/server/config/{ => jwt}/JwtSettings.java (96%) rename application/src/main/java/org/thingsboard/server/config/{ => jwt}/JwtSettingsService.java (74%) diff --git a/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java b/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java index e90ed98351..6009dafafa 100644 --- a/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java +++ b/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java @@ -32,6 +32,7 @@ import java.util.Arrays; "org.thingsboard.server.dao", "org.thingsboard.server.common.stats", "org.thingsboard.server.common.transport.config.ssl", + "org.thingsboard.server.config.jwt", "org.thingsboard.server.cache", "org.thingsboard.server.springfox" }) diff --git a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettings.java similarity index 96% rename from application/src/main/java/org/thingsboard/server/config/JwtSettings.java rename to application/src/main/java/org/thingsboard/server/config/jwt/JwtSettings.java index e5667dc811..f99b36f32e 100644 --- a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettings.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.config; +package org.thingsboard.server.config.jwt; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/application/src/main/java/org/thingsboard/server/config/JwtSettingsService.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java similarity index 74% rename from application/src/main/java/org/thingsboard/server/config/JwtSettingsService.java rename to application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java index bb7d3abc55..bbb1bdba8b 100644 --- a/application/src/main/java/org/thingsboard/server/config/JwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.config; +package org.thingsboard.server.config.jwt; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AdminSettings; @@ -67,11 +68,12 @@ public class JwtSettingsService { } public void createJwtAdminSettings() { + log.debug("Creating JWT admin settings..."); Objects.requireNonNull(jwtSettings, "JWT settings is null"); - if (!isJwtAdminSettingsExists()) { + if (isJwtAdminSettingsNotExists()) { if (hasDefaultTokenSigningKey()) { if (!isAllowedDefaultJwtSigningKey()) { - log.warn("JWT token signing key is default. Generating a new random key"); + log.info("JWT token signing key is default. Generating a new random key"); jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); } } @@ -84,12 +86,17 @@ public class JwtSettingsService { } } - public boolean isJwtAdminSettingsExists() { + public boolean isJwtAdminSettingsNotExists() { return findJwtAdminSettings() == null; } AdminSettings findJwtAdminSettings() { - return adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); + try { + return adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); + } catch (InvalidDataAccessResourceUsageException ignored) { + log.debug("findAdminSettingsByKey is returning InvalidDataAccessResourceUsageException. This is an installation case when the database is not initialized yet"); + return null; + } } /* @@ -101,15 +108,13 @@ public class JwtSettingsService { } public void validateJwtTokenSigningKey() { - if (!isJwtAdminSettingsExists()) { - if (hasDefaultTokenSigningKey()) { - if (isAllowedDefaultJwtSigningKey()) { - log.warn("Default JWT signing key is allowed. This is a security issue. Please, consider to set a strong key in admin settings"); - } else { - String message = "Please, set a unique signing key with env variable JWT_TOKEN_SIGNING_KEY. Key is a Base64 encoded phrase. This will require to generate new tokens for all users and API that uses JWT tokens. To allow insecure JWS use TB_ALLOW_DEFAULT_JWT_SIGNING_KEY=true"; - log.error(message); - throw new ValidationException(message); - } + if (isJwtAdminSettingsNotExists() && hasDefaultTokenSigningKey()) { + if (isAllowedDefaultJwtSigningKey()) { + log.warn("Default JWT signing key is allowed. This is a security issue. Please, consider to set a strong key in admin settings"); + } else { + String message = "Please, set a unique signing key with env variable JWT_TOKEN_SIGNING_KEY. Key is a Base64 encoded phrase. This will require to generate new tokens for all users and API that uses JWT tokens. To allow insecure JWS use TB_ALLOW_DEFAULT_JWT_SIGNING_KEY=true"; + log.error(message); + throw new ValidationException(message); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java index 27117239f0..8dbfaab893 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java @@ -19,7 +19,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import org.thingsboard.server.config.JwtSettingsService; +import org.thingsboard.server.config.jwt.JwtSettingsService; @Service @Profile("install") @@ -31,7 +31,7 @@ public class ConditionValidatorUpgradeServiceImpl implements ConditionValidatorU @Override public void validateConditionsBeforeUpgrade(String fromVersion) throws Exception { - log.info("Validating conditions before upgrade.."); + log.info("Validating conditions before upgrade..."); jwtSettingsService.validateJwtTokenSigningKey(); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 9c9ffb5188..961d0265f9 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -82,7 +82,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.config.JwtSettingsService; +import org.thingsboard.server.config.jwt.JwtSettingsService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceCredentialsService; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java index 73bb97ea0b..fb4cf74ee2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.config.JwtSettingsService; +import org.thingsboard.server.config.jwt.JwtSettingsService; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import javax.annotation.PostConstruct; diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index a2fe509a1a..cf19304ba7 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.config.JwtSettingsService; +import org.thingsboard.server.config.jwt.JwtSettingsService; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index c3223b920e..796ef93c0b 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -23,8 +23,8 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.config.JwtSettings; -import org.thingsboard.server.config.JwtSettingsService; +import org.thingsboard.server.config.jwt.JwtSettings; +import org.thingsboard.server.config.jwt.JwtSettingsService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.AccessJwtToken; diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java index ea5c7f20e8..0ff639d66e 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java @@ -26,8 +26,8 @@ import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.config.JwtSettings; -import org.thingsboard.server.config.JwtSettingsService; +import org.thingsboard.server.config.jwt.JwtSettings; +import org.thingsboard.server.config.jwt.JwtSettingsService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; diff --git a/packaging/java/scripts/install/logback.xml b/packaging/java/scripts/install/logback.xml index 0047956c93..9233ab4d0b 100644 --- a/packaging/java/scripts/install/logback.xml +++ b/packaging/java/scripts/install/logback.xml @@ -56,6 +56,10 @@ + + + + From ef97f1272450b5b4976cc17b43a967943a6c9d42 Mon Sep 17 00:00:00 2001 From: Yuriy Lytvynchuk Date: Wed, 28 Sep 2022 09:55:58 +0300 Subject: [PATCH 007/226] add checkTenantEntity --- .../actors/ruleChain/DefaultTbContext.java | 16 +++++++ .../rule/engine/api/TbContext.java | 2 + .../rule/engine/debug/TbMsgGeneratorNode.java | 1 + .../engine/filter/TbCheckRelationNode.java | 1 + .../util/EntitiesTenantIdAsyncLoader.java | 46 +++++++++++++++++-- 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 08b711015f..d5ff1ff334 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -31,6 +31,7 @@ import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; +import org.thingsboard.rule.engine.util.EntitiesTenantIdAsyncLoader; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.cluster.TbClusterService; @@ -89,6 +90,7 @@ import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import java.util.Collections; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -704,6 +706,20 @@ class DefaultTbContext implements TbContext { return metaData; } + public void checkTenantEntity(EntityId entityId) { + try { + TenantId entityTenantId = EntitiesTenantIdAsyncLoader.findTenantIdByEntityId(this, entityId).get(); + if (entityTenantId == null) { + throw new RuntimeException("Entity with id '" + entityId + "'not found!"); + } + if (!entityTenantId.equals(this.getTenantId())) { + throw new RuntimeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant."); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + private class SimpleTbQueueCallback implements TbQueueCallback { private final Runnable onSuccess; private final Consumer onFailure; diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index f74efe7748..1c2cf92256 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -193,6 +193,8 @@ public interface TbContext { boolean isLocalEntity(EntityId entityId); + void checkTenantEntity(EntityId entityId); + RuleNodeId getSelfId(); RuleNode getSelf(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index 32ec34ef39..4e14a6b15b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -80,6 +80,7 @@ public class TbMsgGeneratorNode implements TbNode { } else { originatorId = ctx.getSelfId(); } + ctx.checkTenantEntity(originatorId); updateGeneratorState(ctx); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java index 4832a0af35..5b38655d9a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java @@ -59,6 +59,7 @@ public class TbCheckRelationNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { this.config = TbNodeUtils.convert(configuration, TbCheckRelationNodeConfiguration.class); + ctx.checkTenantEntity(EntityIdFactory.getByTypeAndId(config.getEntityType(), config.getEntityId())); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java index 4d57e89fda..2699553ff1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java @@ -23,9 +23,15 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; @@ -57,10 +63,42 @@ public class EntitiesTenantIdAsyncLoader { } } + public static ListenableFuture findTenantIdByEntityId(TbContext ctx, EntityId entityId) { + + switch (entityId.getEntityType()) { + case TENANT: + return Futures.immediateFuture((TenantId) entityId); + case CUSTOMER: + return getTenantAsync(ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), (CustomerId) entityId)); + case USER: + return getTenantAsync(ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) entityId)); + case ASSET: + return getTenantAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) entityId)); + case DEVICE: + return getTenantAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), (DeviceId) entityId)); + case ALARM: + return getTenantAsync(ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), (AlarmId) entityId)); + case RULE_CHAIN: + return getTenantAsync(ctx.getRuleChainService().findRuleChainByIdAsync(ctx.getTenantId(), (RuleChainId) entityId)); + case ENTITY_VIEW: + return getTenantAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), (EntityViewId) entityId)); + case DASHBOARD: + return getTenantAsync(ctx.getDashboardService().findDashboardByIdAsync(ctx.getTenantId(), (DashboardId) entityId)); + case EDGE: + return getTenantAsync(ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), (EdgeId) entityId)); + case OTA_PACKAGE: + return getTenantAsync(ctx.getOtaPackageService().findOtaPackageInfoByIdAsync(ctx.getTenantId(), (OtaPackageId) entityId)); + case ASSET_PROFILE: + return getTenantAsync(Futures.immediateFuture(ctx.getAssetProfileCache().get(ctx.getTenantId(), (AssetProfileId) entityId))); + case DEVICE_PROFILE: + return getTenantAsync(Futures.immediateFuture(ctx.getDeviceProfileCache().get(ctx.getTenantId(), (DeviceProfileId) entityId))); + default: + return Futures.immediateFailedFuture(new TbNodeException("Unexpected entityId EntityType " + entityId.getEntityType())); + } + } + private static ListenableFuture getTenantAsync(ListenableFuture future) { - return Futures.transformAsync(future, in -> { - return in != null ? Futures.immediateFuture(in.getTenantId()) - : Futures.immediateFuture(null); - }, MoreExecutors.directExecutor()); + return Futures.transformAsync(future, in -> in != null ? Futures.immediateFuture(in.getTenantId()) + : Futures.immediateFuture(null), MoreExecutors.directExecutor()); } } From 03842f92d9f61e5dd3d03a3cab8714c461b4ea14 Mon Sep 17 00:00:00 2001 From: Yuriy Lytvynchuk Date: Thu, 6 Oct 2022 12:24:53 +0300 Subject: [PATCH 008/226] refactor code --- .../actors/ruleChain/DefaultTbContext.java | 3 +- .../rule/engine/debug/TbMsgGeneratorNode.java | 2 +- .../engine/filter/TbCheckRelationNode.java | 10 +++- .../util/EntitiesTenantIdAsyncLoader.java | 60 ++++++------------- 4 files changed, 27 insertions(+), 48 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index d5ff1ff334..0e557ea76c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -90,7 +90,6 @@ import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import java.util.Collections; import java.util.Set; -import java.util.concurrent.ExecutionException; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -708,7 +707,7 @@ class DefaultTbContext implements TbContext { public void checkTenantEntity(EntityId entityId) { try { - TenantId entityTenantId = EntitiesTenantIdAsyncLoader.findTenantIdByEntityId(this, entityId).get(); + TenantId entityTenantId = EntitiesTenantIdAsyncLoader.findEntityIdAsync(this, entityId).get(); if (entityTenantId == null) { throw new RuntimeException("Entity with id '" + entityId + "'not found!"); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index 4e14a6b15b..a7752aa116 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -77,10 +77,10 @@ public class TbMsgGeneratorNode implements TbNode { this.currentMsgCount = 0; if (!StringUtils.isEmpty(config.getOriginatorId())) { originatorId = EntityIdFactory.getByTypeAndUuid(config.getOriginatorType(), config.getOriginatorId()); + ctx.checkTenantEntity(originatorId); } else { originatorId = ctx.getSelfId(); } - ctx.checkTenantEntity(originatorId); updateGeneratorState(ctx); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java index 5b38655d9a..2dacc1a458 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java @@ -55,11 +55,15 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; public class TbCheckRelationNode implements TbNode { private TbCheckRelationNodeConfiguration config; + private EntityId singleEntityId; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { this.config = TbNodeUtils.convert(configuration, TbCheckRelationNodeConfiguration.class); - ctx.checkTenantEntity(EntityIdFactory.getByTypeAndId(config.getEntityType(), config.getEntityId())); + if (config.isCheckForSingleEntity()) { + this.singleEntityId = EntityIdFactory.getByTypeAndId(config.getEntityType(), config.getEntityId()); + ctx.checkTenantEntity(singleEntityId); + } } @Override @@ -77,10 +81,10 @@ public class TbCheckRelationNode implements TbNode { EntityId from; EntityId to; if (EntitySearchDirection.FROM.name().equals(config.getDirection())) { - from = EntityIdFactory.getByTypeAndId(config.getEntityType(), config.getEntityId()); + from = singleEntityId; to = msg.getOriginator(); } else { - to = EntityIdFactory.getByTypeAndId(config.getEntityType(), config.getEntityId()); + to = singleEntityId; from = msg.getOriginator(); } return ctx.getRelationService().checkRelationAsync(ctx.getTenantId(), from, to, config.getRelationType(), RelationTypeGroup.COMMON); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java index 2699553ff1..a8547bdde4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.util; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.HasTenantId; @@ -37,68 +36,45 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; public class EntitiesTenantIdAsyncLoader { - /** - * @deprecated consider to remove since tenantId is already defined in the TbContext. - */ - @Deprecated - public static ListenableFuture findEntityIdAsync(TbContext ctx, EntityId original) { - switch (original.getEntityType()) { - case TENANT: - return Futures.immediateFuture((TenantId) original); - case CUSTOMER: - return getTenantAsync(ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), (CustomerId) original)); - case USER: - return getTenantAsync(ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) original)); - case ASSET: - return getTenantAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) original)); - case DEVICE: - return getTenantAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), (DeviceId) original)); - case ALARM: - return getTenantAsync(ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), (AlarmId) original)); - case RULE_CHAIN: - return getTenantAsync(ctx.getRuleChainService().findRuleChainByIdAsync(ctx.getTenantId(), (RuleChainId) original)); - default: - return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original.getEntityType())); - } - } - - public static ListenableFuture findTenantIdByEntityId(TbContext ctx, EntityId entityId) { + public static ListenableFuture findEntityIdAsync(TbContext ctx, EntityId entityId) { switch (entityId.getEntityType()) { case TENANT: return Futures.immediateFuture((TenantId) entityId); case CUSTOMER: - return getTenantAsync(ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), (CustomerId) entityId)); + return getTenantAsync(ctx, ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), (CustomerId) entityId)); case USER: - return getTenantAsync(ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) entityId)); + return getTenantAsync(ctx, ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) entityId)); case ASSET: - return getTenantAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) entityId)); + return getTenantAsync(ctx, ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) entityId)); case DEVICE: - return getTenantAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), (DeviceId) entityId)); + return getTenantAsync(ctx, ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), (DeviceId) entityId)); case ALARM: - return getTenantAsync(ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), (AlarmId) entityId)); + return getTenantAsync(ctx, ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), (AlarmId) entityId)); case RULE_CHAIN: - return getTenantAsync(ctx.getRuleChainService().findRuleChainByIdAsync(ctx.getTenantId(), (RuleChainId) entityId)); + return getTenantAsync(ctx, ctx.getRuleChainService().findRuleChainByIdAsync(ctx.getTenantId(), (RuleChainId) entityId)); case ENTITY_VIEW: - return getTenantAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), (EntityViewId) entityId)); + return getTenantAsync(ctx, ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), (EntityViewId) entityId)); case DASHBOARD: - return getTenantAsync(ctx.getDashboardService().findDashboardByIdAsync(ctx.getTenantId(), (DashboardId) entityId)); + return getTenantAsync(ctx, ctx.getDashboardService().findDashboardByIdAsync(ctx.getTenantId(), (DashboardId) entityId)); case EDGE: - return getTenantAsync(ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), (EdgeId) entityId)); + return getTenantAsync(ctx, ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), (EdgeId) entityId)); case OTA_PACKAGE: - return getTenantAsync(ctx.getOtaPackageService().findOtaPackageInfoByIdAsync(ctx.getTenantId(), (OtaPackageId) entityId)); + return getTenantAsync(ctx, ctx.getOtaPackageService().findOtaPackageInfoByIdAsync(ctx.getTenantId(), (OtaPackageId) entityId)); case ASSET_PROFILE: - return getTenantAsync(Futures.immediateFuture(ctx.getAssetProfileCache().get(ctx.getTenantId(), (AssetProfileId) entityId))); + return getTenantAsync(ctx, Futures.immediateFuture(ctx.getAssetProfileCache().get(ctx.getTenantId(), (AssetProfileId) entityId))); case DEVICE_PROFILE: - return getTenantAsync(Futures.immediateFuture(ctx.getDeviceProfileCache().get(ctx.getTenantId(), (DeviceProfileId) entityId))); + return getTenantAsync(ctx, Futures.immediateFuture(ctx.getDeviceProfileCache().get(ctx.getTenantId(), (DeviceProfileId) entityId))); default: return Futures.immediateFailedFuture(new TbNodeException("Unexpected entityId EntityType " + entityId.getEntityType())); } } - private static ListenableFuture getTenantAsync(ListenableFuture future) { - return Futures.transformAsync(future, in -> in != null ? Futures.immediateFuture(in.getTenantId()) - : Futures.immediateFuture(null), MoreExecutors.directExecutor()); + private static ListenableFuture getTenantAsync(TbContext ctx, ListenableFuture future) { + return Futures.transformAsync(future, in -> { + return in != null ? Futures.immediateFuture(in.getTenantId()) + : Futures.immediateFuture(null); + }, ctx.getDbCallbackExecutor()); } } From 6edec4a94cf554c28cadc37a4337dba411ce4b57 Mon Sep 17 00:00:00 2001 From: Yuriy Lytvynchuk Date: Fri, 7 Oct 2022 11:01:55 +0300 Subject: [PATCH 009/226] 1. rename checkTenantEntity -> isTenantEntity 2. Refactor code --- .../actors/ruleChain/DefaultTbContext.java | 2 +- .../rule/engine/api/TbContext.java | 3 +- .../rule/engine/debug/TbMsgGeneratorNode.java | 2 +- .../engine/filter/TbCheckRelationNode.java | 2 +- .../util/EntitiesTenantIdAsyncLoader.java | 38 ++++++++++--------- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 0e557ea76c..239466fe81 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -705,7 +705,7 @@ class DefaultTbContext implements TbContext { return metaData; } - public void checkTenantEntity(EntityId entityId) { + public void isTenantEntity(EntityId entityId) { try { TenantId entityTenantId = EntitiesTenantIdAsyncLoader.findEntityIdAsync(this, entityId).get(); if (entityTenantId == null) { diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 1c2cf92256..5892633649 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -193,7 +192,7 @@ public interface TbContext { boolean isLocalEntity(EntityId entityId); - void checkTenantEntity(EntityId entityId); + void isTenantEntity(EntityId entityId); RuleNodeId getSelfId(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index a7752aa116..12614f191c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -77,7 +77,7 @@ public class TbMsgGeneratorNode implements TbNode { this.currentMsgCount = 0; if (!StringUtils.isEmpty(config.getOriginatorId())) { originatorId = EntityIdFactory.getByTypeAndUuid(config.getOriginatorType(), config.getOriginatorId()); - ctx.checkTenantEntity(originatorId); + ctx.isTenantEntity(originatorId); } else { originatorId = ctx.getSelfId(); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java index 2dacc1a458..c6707cbd1e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java @@ -62,7 +62,7 @@ public class TbCheckRelationNode implements TbNode { this.config = TbNodeUtils.convert(configuration, TbCheckRelationNodeConfiguration.class); if (config.isCheckForSingleEntity()) { this.singleEntityId = EntityIdFactory.getByTypeAndId(config.getEntityType(), config.getEntityId()); - ctx.checkTenantEntity(singleEntityId); + ctx.isTenantEntity(singleEntityId); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java index a8547bdde4..6e403af86b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.util; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.HasTenantId; @@ -37,44 +38,45 @@ import org.thingsboard.server.common.data.id.UserId; public class EntitiesTenantIdAsyncLoader { - public static ListenableFuture findEntityIdAsync(TbContext ctx, EntityId entityId) { + public static ListenableFuture findEntityIdAsync(TbContext ctx, EntityId original) { - switch (entityId.getEntityType()) { + ListeningExecutor executor = ctx.getDbCallbackExecutor(); + switch (original.getEntityType()) { case TENANT: - return Futures.immediateFuture((TenantId) entityId); + return Futures.immediateFuture((TenantId) original); case CUSTOMER: - return getTenantAsync(ctx, ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), (CustomerId) entityId)); + return getTenantAsync(ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), (CustomerId) original), executor); case USER: - return getTenantAsync(ctx, ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) entityId)); + return getTenantAsync(ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) original), executor); case ASSET: - return getTenantAsync(ctx, ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) entityId)); + return getTenantAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) original), executor); case DEVICE: - return getTenantAsync(ctx, ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), (DeviceId) entityId)); + return getTenantAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), (DeviceId) original), executor); case ALARM: - return getTenantAsync(ctx, ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), (AlarmId) entityId)); + return getTenantAsync(ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), (AlarmId) original), executor); case RULE_CHAIN: - return getTenantAsync(ctx, ctx.getRuleChainService().findRuleChainByIdAsync(ctx.getTenantId(), (RuleChainId) entityId)); + return getTenantAsync(ctx.getRuleChainService().findRuleChainByIdAsync(ctx.getTenantId(), (RuleChainId) original), executor); case ENTITY_VIEW: - return getTenantAsync(ctx, ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), (EntityViewId) entityId)); + return getTenantAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), (EntityViewId) original), executor); case DASHBOARD: - return getTenantAsync(ctx, ctx.getDashboardService().findDashboardByIdAsync(ctx.getTenantId(), (DashboardId) entityId)); + return getTenantAsync(ctx.getDashboardService().findDashboardByIdAsync(ctx.getTenantId(), (DashboardId) original), executor); case EDGE: - return getTenantAsync(ctx, ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), (EdgeId) entityId)); + return getTenantAsync(ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), (EdgeId) original), executor); case OTA_PACKAGE: - return getTenantAsync(ctx, ctx.getOtaPackageService().findOtaPackageInfoByIdAsync(ctx.getTenantId(), (OtaPackageId) entityId)); + return getTenantAsync(ctx.getOtaPackageService().findOtaPackageInfoByIdAsync(ctx.getTenantId(), (OtaPackageId) original), executor); case ASSET_PROFILE: - return getTenantAsync(ctx, Futures.immediateFuture(ctx.getAssetProfileCache().get(ctx.getTenantId(), (AssetProfileId) entityId))); + return getTenantAsync(Futures.immediateFuture(ctx.getAssetProfileCache().get(ctx.getTenantId(), (AssetProfileId) original)), executor); case DEVICE_PROFILE: - return getTenantAsync(ctx, Futures.immediateFuture(ctx.getDeviceProfileCache().get(ctx.getTenantId(), (DeviceProfileId) entityId))); + return getTenantAsync(Futures.immediateFuture(ctx.getDeviceProfileCache().get(ctx.getTenantId(), (DeviceProfileId) original)), executor); default: - return Futures.immediateFailedFuture(new TbNodeException("Unexpected entityId EntityType " + entityId.getEntityType())); + return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original.getEntityType())); } } - private static ListenableFuture getTenantAsync(TbContext ctx, ListenableFuture future) { + private static ListenableFuture getTenantAsync(ListenableFuture future, ListeningExecutor executor) { return Futures.transformAsync(future, in -> { return in != null ? Futures.immediateFuture(in.getTenantId()) : Futures.immediateFuture(null); - }, ctx.getDbCallbackExecutor()); + }, executor); } } From 030981cedf01daf0b10c0a8dd2317e0d585419a7 Mon Sep 17 00:00:00 2001 From: Yuriy Lytvynchuk Date: Fri, 7 Oct 2022 11:05:51 +0300 Subject: [PATCH 010/226] delete empty row --- .../rule/engine/util/EntitiesTenantIdAsyncLoader.java | 1 - 1 file changed, 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java index 6e403af86b..752ebf5143 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java @@ -39,7 +39,6 @@ import org.thingsboard.server.common.data.id.UserId; public class EntitiesTenantIdAsyncLoader { public static ListenableFuture findEntityIdAsync(TbContext ctx, EntityId original) { - ListeningExecutor executor = ctx.getDbCallbackExecutor(); switch (original.getEntityType()) { case TENANT: From 126d7215c5cc9321030e9f8147dad1753dd3dcd6 Mon Sep 17 00:00:00 2001 From: oyurov Date: Sun, 16 Oct 2022 15:50:42 +0200 Subject: [PATCH 011/226] Added generation an audit log for oauth2 login and with which provider was done an authorization --- .../server/controller/AuthController.java | 45 +------------ .../controller/TwoFactorAuthController.java | 6 +- .../Oauth2AuthenticationSuccessHandler.java | 3 + .../auth/rest/RestAuthenticationProvider.java | 8 ++- .../system/DefaultSystemSecurityService.java | 50 ++++----------- .../system/SystemSecurityService.java | 2 +- .../server/utils/AuthorizationDetails.java | 28 +++++++++ .../utils/RestAuthenticationDetailsUtils.java | 63 +++++++++++++++++++ .../server/dao/audit/AuditLogServiceImpl.java | 2 + 9 files changed, 118 insertions(+), 89 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/utils/AuthorizationDetails.java create mode 100644 application/src/main/java/org/thingsboard/server/utils/RestAuthenticationDetailsUtils.java diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 72be55a135..ba34edef81 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -59,7 +59,6 @@ import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.system.SystemSecurityService; -import ua_parser.Client; import javax.servlet.http.HttpServletRequest; import java.net.URI; @@ -324,49 +323,7 @@ public class AuthController extends BaseController { private void logLogoutAction(HttpServletRequest request) throws ThingsboardException { try { - SecurityUser user = getCurrentUser(); - RestAuthenticationDetails details = new RestAuthenticationDetails(request); - String clientAddress = details.getClientAddress(); - String browser = "Unknown"; - String os = "Unknown"; - String device = "Unknown"; - if (details.getUserAgent() != null) { - Client userAgent = details.getUserAgent(); - if (userAgent.userAgent != null) { - browser = userAgent.userAgent.family; - if (userAgent.userAgent.major != null) { - browser += " " + userAgent.userAgent.major; - if (userAgent.userAgent.minor != null) { - browser += "." + userAgent.userAgent.minor; - if (userAgent.userAgent.patch != null) { - browser += "." + userAgent.userAgent.patch; - } - } - } - } - if (userAgent.os != null) { - os = userAgent.os.family; - if (userAgent.os.major != null) { - os += " " + userAgent.os.major; - if (userAgent.os.minor != null) { - os += "." + userAgent.os.minor; - if (userAgent.os.patch != null) { - os += "." + userAgent.os.patch; - if (userAgent.os.patchMinor != null) { - os += "." + userAgent.os.patchMinor; - } - } - } - } - } - if (userAgent.device != null) { - device = userAgent.device.family; - } - } - auditLogService.logEntityAction( - user.getTenantId(), user.getCustomerId(), user.getId(), - user.getName(), user.getId(), null, ActionType.LOGOUT, null, clientAddress, browser, os, device); - + systemSecurityService.logLoginAction(getCurrentUser(), new RestAuthenticationDetails(request), ActionType.LOGOUT, null, "REST"); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index 003b4ab450..122d3e85a6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -58,6 +58,8 @@ import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; @RequiredArgsConstructor public class TwoFactorAuthController extends BaseController { + private static final String TwoFA_PROVIDER = "2FA "; + private final TwoFactorAuthService twoFactorAuthService; private final TwoFaConfigManager twoFaConfigManager; private final JwtTokenFactory tokenFactory; @@ -92,12 +94,12 @@ public class TwoFactorAuthController extends BaseController { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true); if (verificationSuccess) { - systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null); + systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null, TwoFA_PROVIDER + providerType); user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal()); return tokenFactory.createTokenPair(user); } else { ThingsboardException error = new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.BAD_REQUEST_PARAMS); - systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error); + systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error, TwoFA_PROVIDER + providerType); throw error; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index e2a78eb605..e5e24a9d87 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -25,6 +25,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -33,6 +34,7 @@ import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; +import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.system.SystemSecurityService; @@ -102,6 +104,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS clearAuthenticationAttributes(request, response); getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken()); + systemSecurityService.logLoginAction(securityUser, new RestAuthenticationDetails(request), ActionType.LOGIN, null, "OAUTH2: " + registration.getName()); } catch (Exception e) { log.debug("Error occurred during processing authentication success result. " + "request [{}], response [{}], authentication [{}]", request, response, authentication, e); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java index b3ef88dbff..1f8a49b803 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java @@ -53,6 +53,8 @@ import java.util.UUID; @TbCoreComponent public class RestAuthenticationProvider implements AuthenticationProvider { + private static final String REST_PROVIDER = "REST"; + private final SystemSecurityService systemSecurityService; private final UserService userService; private final CustomerService customerService; @@ -87,7 +89,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider { if (twoFactorAuthService.isTwoFaEnabled(securityUser.getTenantId(), securityUser.getId())) { return new MfaAuthenticationToken(securityUser); } else { - systemSecurityService.logLoginAction(securityUser, authentication.getDetails(), ActionType.LOGIN, null); + systemSecurityService.logLoginAction(securityUser, authentication.getDetails(), ActionType.LOGIN, null, REST_PROVIDER); } } else { String publicId = userPrincipal.getValue(); @@ -113,7 +115,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider { try { systemSecurityService.validateUserCredentials(user.getTenantId(), userCredentials, username, password); } catch (LockedException e) { - systemSecurityService.logLoginAction(user, authentication.getDetails(), ActionType.LOCKOUT, null); + systemSecurityService.logLoginAction(user, authentication.getDetails(), ActionType.LOCKOUT, null, REST_PROVIDER); throw e; } @@ -122,7 +124,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider { return new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); } catch (Exception e) { - systemSecurityService.logLoginAction(user, authentication.getDetails(), ActionType.LOGIN, e); + systemSecurityService.logLoginAction(user, authentication.getDetails(), ActionType.LOGIN, e, REST_PROVIDER); throw e; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index 5e8965c140..1bd3ba5b88 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -59,8 +59,9 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.utils.AuthorizationDetails; import org.thingsboard.server.utils.MiscUtils; -import ua_parser.Client; +import org.thingsboard.server.utils.RestAuthenticationDetailsUtils; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; @@ -232,7 +233,8 @@ public class DefaultSystemSecurityService implements SystemSecurityService { JsonNode additionalInfo = user.getAdditionalInfo(); if (additionalInfo instanceof ObjectNode && additionalInfo.has(UserServiceImpl.USER_PASSWORD_HISTORY)) { JsonNode userPasswordHistoryJson = additionalInfo.get(UserServiceImpl.USER_PASSWORD_HISTORY); - Map userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() {}); + Map userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() { + }); for (Map.Entry entry : userPasswordHistoryMap.entrySet()) { if (encoder.matches(password, entry.getValue()) && Long.parseLong(entry.getKey()) > passwordReuseFrequencyTs) { throw new DataValidationException("Password was already used for the last " + passwordPolicy.getPasswordReuseFrequencyDays() + " days"); @@ -262,54 +264,24 @@ public class DefaultSystemSecurityService implements SystemSecurityService { } @Override - public void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e) { + public void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e, String provider) { String clientAddress = "Unknown"; String browser = "Unknown"; String os = "Unknown"; String device = "Unknown"; if (authenticationDetails instanceof RestAuthenticationDetails) { - RestAuthenticationDetails details = (RestAuthenticationDetails) authenticationDetails; + AuthorizationDetails details = RestAuthenticationDetailsUtils.getRestAuthenticationDetails((RestAuthenticationDetails) authenticationDetails); clientAddress = details.getClientAddress(); - if (details.getUserAgent() != null) { - Client userAgent = details.getUserAgent(); - if (userAgent.userAgent != null) { - browser = userAgent.userAgent.family; - if (userAgent.userAgent.major != null) { - browser += " " + userAgent.userAgent.major; - if (userAgent.userAgent.minor != null) { - browser += "." + userAgent.userAgent.minor; - if (userAgent.userAgent.patch != null) { - browser += "." + userAgent.userAgent.patch; - } - } - } - } - if (userAgent.os != null) { - os = userAgent.os.family; - if (userAgent.os.major != null) { - os += " " + userAgent.os.major; - if (userAgent.os.minor != null) { - os += "." + userAgent.os.minor; - if (userAgent.os.patch != null) { - os += "." + userAgent.os.patch; - if (userAgent.os.patchMinor != null) { - os += "." + userAgent.os.patchMinor; - } - } - } - } - } - if (userAgent.device != null) { - device = userAgent.device.family; - } - } - } + browser = details.getBrowser(); + os = details.getOs(); + device = details.getDevice(); + } if (actionType == ActionType.LOGIN && e == null) { userService.setLastLoginTs(user.getTenantId(), user.getId()); } auditLogService.logEntityAction( user.getTenantId(), user.getCustomerId(), user.getId(), - user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device); + user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device, provider); } private static boolean isPositiveInteger(Integer val) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java index 6173d408c5..9dbb0d8630 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java @@ -42,6 +42,6 @@ public interface SystemSecurityService { String getBaseUrl(TenantId tenantId, CustomerId customerId, HttpServletRequest httpServletRequest); - void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e); + void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e, String provider); } diff --git a/application/src/main/java/org/thingsboard/server/utils/AuthorizationDetails.java b/application/src/main/java/org/thingsboard/server/utils/AuthorizationDetails.java new file mode 100644 index 0000000000..36f95c0bbf --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/utils/AuthorizationDetails.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2022 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.utils; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class AuthorizationDetails { + private String clientAddress; + private String browser; + private String os; + private String device; +} diff --git a/application/src/main/java/org/thingsboard/server/utils/RestAuthenticationDetailsUtils.java b/application/src/main/java/org/thingsboard/server/utils/RestAuthenticationDetailsUtils.java new file mode 100644 index 0000000000..d39d15ff63 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/utils/RestAuthenticationDetailsUtils.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2022 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.utils; + +import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; +import ua_parser.Client; + +public class RestAuthenticationDetailsUtils { + + public static AuthorizationDetails getRestAuthenticationDetails(RestAuthenticationDetails details) { + String clientAddress = details.getClientAddress(); + String browser = "Unknown"; + String os = "Unknown"; + String device = "Unknown"; + if (details.getUserAgent() != null) { + Client userAgent = details.getUserAgent(); + if (userAgent.userAgent != null) { + browser = userAgent.userAgent.family; + if (userAgent.userAgent.major != null) { + browser += " " + userAgent.userAgent.major; + if (userAgent.userAgent.minor != null) { + browser += "." + userAgent.userAgent.minor; + if (userAgent.userAgent.patch != null) { + browser += "." + userAgent.userAgent.patch; + } + } + } + } + if (userAgent.os != null) { + os = userAgent.os.family; + if (userAgent.os.major != null) { + os += " " + userAgent.os.major; + if (userAgent.os.minor != null) { + os += "." + userAgent.os.minor; + if (userAgent.os.patch != null) { + os += "." + userAgent.os.patch; + if (userAgent.os.patchMinor != null) { + os += "." + userAgent.os.patchMinor; + } + } + } + } + } + if (userAgent.device != null) { + device = userAgent.device.family; + } + } + return new AuthorizationDetails(clientAddress, browser, os, device); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index da94141768..aa9c669ca5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -257,10 +257,12 @@ public class AuditLogServiceImpl implements AuditLogService { String browser = extractParameter(String.class, 1, additionalInfo); String os = extractParameter(String.class, 2, additionalInfo); String device = extractParameter(String.class, 3, additionalInfo); + String provider = extractParameter(String.class, 4, additionalInfo); actionData.put("clientAddress", clientAddress); actionData.put("browser", browser); actionData.put("os", os); actionData.put("device", device); + actionData.put("provider", provider); break; case PROVISION_SUCCESS: case PROVISION_FAILURE: From e1ab81c6fc857fe4bd577120e2fda8c55faf3421 Mon Sep 17 00:00:00 2001 From: oyurov Date: Sun, 16 Oct 2022 17:23:31 +0200 Subject: [PATCH 012/226] Implemented possibility to outdate user jwt token after logout --- .../java/org/thingsboard/server/controller/AuthController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 72be55a135..b3d9cb6634 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -367,6 +367,7 @@ public class AuthController extends BaseController { user.getTenantId(), user.getCustomerId(), user.getId(), user.getName(), user.getId(), null, ActionType.LOGOUT, null, clientAddress, browser, os, device); + eventPublisher.publishEvent(new UserAuthDataChangedEvent(user.getId())); } catch (Exception e) { throw handleException(e); } From 3d7b829a9994089e91f54f39751490d70bd55c26 Mon Sep 17 00:00:00 2001 From: oyurov Date: Mon, 17 Oct 2022 13:23:12 +0200 Subject: [PATCH 013/226] Refactor tests to support outdate tokens after logout --- .../server/controller/AbstractWebTest.java | 12 ++++++++---- .../server/controller/BaseAlarmControllerTest.java | 4 ++-- .../server/controller/BaseAuthControllerTest.java | 2 ++ .../server/controller/BaseUserControllerTest.java | 8 ++++---- 4 files changed, 16 insertions(+), 10 deletions(-) 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 3783b746f3..ff079fc731 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -234,7 +234,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { customerUser = createUserAndLogin(customerUser, CUSTOMER_USER_PASSWORD); customerUserId = customerUser.getId(); - logout(); + resetTokens(); log.info("Executed web test setup"); } @@ -336,7 +336,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { Assert.assertNotNull(savedDifferentCustomer); differentCustomerId = savedDifferentCustomer.getId(); - logout(); + resetTokens(); } protected void deleteDifferentTenant() throws Exception { @@ -350,7 +350,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected User createUserAndLogin(User user, String password) throws Exception { User savedUser = doPost("/api/user", user, User.class); - logout(); + resetTokens(); JsonNode activateRequest = getActivateRequest(password); JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class); validateAndSetJwtToken(tokenInfo, user.getEmail()); @@ -413,12 +413,16 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { Assert.assertEquals(username, subject); } - protected void logout() throws Exception { + protected void resetTokens() throws Exception { this.token = null; this.refreshToken = null; this.username = null; } + protected void logout() throws Exception { + doPost("/api/auth/logout"); + } + protected void setJwtToken(MockHttpServletRequestBuilder request) { if (this.token != null) { request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java index 4419e0e425..493b1818d9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java @@ -77,7 +77,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { device.setCustomerId(customerId); customerDevice = doPost("/api/device", device, Device.class); - logout(); + resetTokens(); } @After @@ -423,7 +423,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { testNotifyEntityNeverMsgToEdgeServiceOneTime(alarm, alarm.getId(), tenantId, ActionType.ADDED); - logout(); + resetTokens(); JsonNode publicLoginRequest = JacksonUtil.toJsonNode("{\"publicId\": \"" + publicId + "\"}"); JsonNode tokens = doPost("/api/auth/login/public", publicLoginRequest, JsonNode.class); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java index f64cac0f99..61945e59bb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java @@ -60,6 +60,8 @@ public abstract class BaseAuthControllerTest extends AbstractControllerTest { logout(); doGet("/api/auth/user") .andExpect(status().isUnauthorized()); + + resetTokens(); } @Test diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java index 9c01f4323d..2f1dc97f14 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java @@ -106,7 +106,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { ActionType.ADDED, ActionType.ADDED, 1, 1, 1); Mockito.reset(tbClusterService, auditLogService); - logout(); + resetTokens(); doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken) .andExpect(status().isSeeOther()) .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken)); @@ -123,7 +123,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { .andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name()))) .andExpect(jsonPath("$.email", is(email))); - logout(); + resetTokens(); login(email, "testPassword"); @@ -218,7 +218,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { user.setLastName("Downs"); User savedUser = createUserAndLogin(user, "testPassword1"); - logout(); + resetTokens(); JsonNode resetPasswordByEmailRequest = new ObjectMapper().createObjectNode() .put("email", email); @@ -244,7 +244,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { .andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name()))) .andExpect(jsonPath("$.email", is(email))); - logout(); + resetTokens(); login(email, "testPassword2"); doGet("/api/auth/user") From aaa8dbbfddb225f7175ab7272ae70f833063c4af Mon Sep 17 00:00:00 2001 From: oyurov Date: Tue, 18 Oct 2022 14:04:26 +0200 Subject: [PATCH 014/226] Fix test --- .../thingsboard/server/controller/AbstractWebTest.java | 2 +- .../server/controller/BaseAuthControllerTest.java | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) 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 ff079fc731..a3b144f4a9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -420,7 +420,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } protected void logout() throws Exception { - doPost("/api/auth/logout"); + doPost("/api/auth/logout").andExpect(status().isOk()); } protected void setJwtToken(MockHttpServletRequestBuilder request) { diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java index 61945e59bb..627ce1a02b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java @@ -15,13 +15,15 @@ */ package org.thingsboard.server.controller; +import org.junit.Test; +import org.thingsboard.server.common.data.security.Authority; + +import java.util.concurrent.TimeUnit; + import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.thingsboard.server.common.data.security.Authority; -import org.junit.Test; - public abstract class BaseAuthControllerTest extends AbstractControllerTest { @Test @@ -57,6 +59,8 @@ public abstract class BaseAuthControllerTest extends AbstractControllerTest { .andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name()))) .andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL))); + TimeUnit.SECONDS.sleep(1); //We need to make sure that event for invalidating token was successfully processed + logout(); doGet("/api/auth/user") .andExpect(status().isUnauthorized()); From df732a8f58f51c9b400e5c7baf6809ef110a8f6d Mon Sep 17 00:00:00 2001 From: oyurov Date: Tue, 18 Oct 2022 20:25:20 +0200 Subject: [PATCH 015/226] Refactor --- .../server/controller/AuthController.java | 2 +- .../controller/TwoFactorAuthController.java | 6 +- .../oauth2/AbstractOAuth2ClientMapper.java | 8 +++ .../Oauth2AuthenticationSuccessHandler.java | 2 +- .../auth/rest/RestAuthenticationProvider.java | 8 +-- .../system/DefaultSystemSecurityService.java | 45 ++++++++++--- .../system/SystemSecurityService.java | 2 +- .../server/utils/AuthorizationDetails.java | 28 --------- .../utils/RestAuthenticationDetailsUtils.java | 63 ------------------- .../server/dao/audit/AuditLogServiceImpl.java | 2 - 10 files changed, 53 insertions(+), 113 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/utils/AuthorizationDetails.java delete mode 100644 application/src/main/java/org/thingsboard/server/utils/RestAuthenticationDetailsUtils.java diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index ba34edef81..6ce85c6d5c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -323,7 +323,7 @@ public class AuthController extends BaseController { private void logLogoutAction(HttpServletRequest request) throws ThingsboardException { try { - systemSecurityService.logLoginAction(getCurrentUser(), new RestAuthenticationDetails(request), ActionType.LOGOUT, null, "REST"); + systemSecurityService.logLoginAction(getCurrentUser(), new RestAuthenticationDetails(request), ActionType.LOGOUT, null); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index 122d3e85a6..003b4ab450 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -58,8 +58,6 @@ import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; @RequiredArgsConstructor public class TwoFactorAuthController extends BaseController { - private static final String TwoFA_PROVIDER = "2FA "; - private final TwoFactorAuthService twoFactorAuthService; private final TwoFaConfigManager twoFaConfigManager; private final JwtTokenFactory tokenFactory; @@ -94,12 +92,12 @@ public class TwoFactorAuthController extends BaseController { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true); if (verificationSuccess) { - systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null, TwoFA_PROVIDER + providerType); + systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null); user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal()); return tokenFactory.createTokenPair(user); } else { ThingsboardException error = new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.BAD_REQUEST_PARAMS); - systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error, TwoFA_PROVIDER + providerType); + systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error); throw error; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java index cbb5fa7306..b32e762752 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.IdBased; @@ -47,6 +48,7 @@ import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.service.entitiy.TbNotificationEntityService; import org.thingsboard.server.service.install.InstallScripts; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -81,6 +83,9 @@ public abstract class AbstractOAuth2ClientMapper { @Autowired private InstallScripts installScripts; + @Autowired + private TbNotificationEntityService notificationEntityService; + @Autowired protected TbTenantProfileCache tenantProfileCache; @@ -151,6 +156,9 @@ public abstract class AbstractOAuth2ClientMapper { UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()); userService.activateUserCredentials(user.getTenantId(), userCredentials.getActivateToken(), passwordEncoder.encode("")); } + + notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, customerId, user.getId(), + user, user, ActionType.ADDED, true, null); } } catch (Exception e) { log.error("Can't get or create security user from oauth2 user", e); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index e5e24a9d87..d41fba4ee5 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -104,7 +104,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS clearAuthenticationAttributes(request, response); getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken()); - systemSecurityService.logLoginAction(securityUser, new RestAuthenticationDetails(request), ActionType.LOGIN, null, "OAUTH2: " + registration.getName()); + systemSecurityService.logLoginAction(securityUser, new RestAuthenticationDetails(request), ActionType.LOGIN, null); } catch (Exception e) { log.debug("Error occurred during processing authentication success result. " + "request [{}], response [{}], authentication [{}]", request, response, authentication, e); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java index 1f8a49b803..b3ef88dbff 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java @@ -53,8 +53,6 @@ import java.util.UUID; @TbCoreComponent public class RestAuthenticationProvider implements AuthenticationProvider { - private static final String REST_PROVIDER = "REST"; - private final SystemSecurityService systemSecurityService; private final UserService userService; private final CustomerService customerService; @@ -89,7 +87,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider { if (twoFactorAuthService.isTwoFaEnabled(securityUser.getTenantId(), securityUser.getId())) { return new MfaAuthenticationToken(securityUser); } else { - systemSecurityService.logLoginAction(securityUser, authentication.getDetails(), ActionType.LOGIN, null, REST_PROVIDER); + systemSecurityService.logLoginAction(securityUser, authentication.getDetails(), ActionType.LOGIN, null); } } else { String publicId = userPrincipal.getValue(); @@ -115,7 +113,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider { try { systemSecurityService.validateUserCredentials(user.getTenantId(), userCredentials, username, password); } catch (LockedException e) { - systemSecurityService.logLoginAction(user, authentication.getDetails(), ActionType.LOCKOUT, null, REST_PROVIDER); + systemSecurityService.logLoginAction(user, authentication.getDetails(), ActionType.LOCKOUT, null); throw e; } @@ -124,7 +122,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider { return new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); } catch (Exception e) { - systemSecurityService.logLoginAction(user, authentication.getDetails(), ActionType.LOGIN, e, REST_PROVIDER); + systemSecurityService.logLoginAction(user, authentication.getDetails(), ActionType.LOGIN, e); throw e; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index 1bd3ba5b88..e7743f8218 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -59,9 +59,8 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; import org.thingsboard.server.service.security.model.SecurityUser; -import org.thingsboard.server.utils.AuthorizationDetails; import org.thingsboard.server.utils.MiscUtils; -import org.thingsboard.server.utils.RestAuthenticationDetailsUtils; +import ua_parser.Client; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; @@ -264,24 +263,54 @@ public class DefaultSystemSecurityService implements SystemSecurityService { } @Override - public void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e, String provider) { + public void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e) { String clientAddress = "Unknown"; String browser = "Unknown"; String os = "Unknown"; String device = "Unknown"; if (authenticationDetails instanceof RestAuthenticationDetails) { - AuthorizationDetails details = RestAuthenticationDetailsUtils.getRestAuthenticationDetails((RestAuthenticationDetails) authenticationDetails); + RestAuthenticationDetails details = (RestAuthenticationDetails) authenticationDetails; clientAddress = details.getClientAddress(); - browser = details.getBrowser(); - os = details.getOs(); - device = details.getDevice(); + if (details.getUserAgent() != null) { + Client userAgent = details.getUserAgent(); + if (userAgent.userAgent != null) { + browser = userAgent.userAgent.family; + if (userAgent.userAgent.major != null) { + browser += " " + userAgent.userAgent.major; + if (userAgent.userAgent.minor != null) { + browser += "." + userAgent.userAgent.minor; + if (userAgent.userAgent.patch != null) { + browser += "." + userAgent.userAgent.patch; + } + } + } + } + if (userAgent.os != null) { + os = userAgent.os.family; + if (userAgent.os.major != null) { + os += " " + userAgent.os.major; + if (userAgent.os.minor != null) { + os += "." + userAgent.os.minor; + if (userAgent.os.patch != null) { + os += "." + userAgent.os.patch; + if (userAgent.os.patchMinor != null) { + os += "." + userAgent.os.patchMinor; + } + } + } + } + } + if (userAgent.device != null) { + device = userAgent.device.family; + } + } } if (actionType == ActionType.LOGIN && e == null) { userService.setLastLoginTs(user.getTenantId(), user.getId()); } auditLogService.logEntityAction( user.getTenantId(), user.getCustomerId(), user.getId(), - user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device, provider); + user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device); } private static boolean isPositiveInteger(Integer val) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java index 9dbb0d8630..6173d408c5 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java @@ -42,6 +42,6 @@ public interface SystemSecurityService { String getBaseUrl(TenantId tenantId, CustomerId customerId, HttpServletRequest httpServletRequest); - void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e, String provider); + void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e); } diff --git a/application/src/main/java/org/thingsboard/server/utils/AuthorizationDetails.java b/application/src/main/java/org/thingsboard/server/utils/AuthorizationDetails.java deleted file mode 100644 index 36f95c0bbf..0000000000 --- a/application/src/main/java/org/thingsboard/server/utils/AuthorizationDetails.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright © 2016-2022 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.utils; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class AuthorizationDetails { - private String clientAddress; - private String browser; - private String os; - private String device; -} diff --git a/application/src/main/java/org/thingsboard/server/utils/RestAuthenticationDetailsUtils.java b/application/src/main/java/org/thingsboard/server/utils/RestAuthenticationDetailsUtils.java deleted file mode 100644 index d39d15ff63..0000000000 --- a/application/src/main/java/org/thingsboard/server/utils/RestAuthenticationDetailsUtils.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright © 2016-2022 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.utils; - -import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; -import ua_parser.Client; - -public class RestAuthenticationDetailsUtils { - - public static AuthorizationDetails getRestAuthenticationDetails(RestAuthenticationDetails details) { - String clientAddress = details.getClientAddress(); - String browser = "Unknown"; - String os = "Unknown"; - String device = "Unknown"; - if (details.getUserAgent() != null) { - Client userAgent = details.getUserAgent(); - if (userAgent.userAgent != null) { - browser = userAgent.userAgent.family; - if (userAgent.userAgent.major != null) { - browser += " " + userAgent.userAgent.major; - if (userAgent.userAgent.minor != null) { - browser += "." + userAgent.userAgent.minor; - if (userAgent.userAgent.patch != null) { - browser += "." + userAgent.userAgent.patch; - } - } - } - } - if (userAgent.os != null) { - os = userAgent.os.family; - if (userAgent.os.major != null) { - os += " " + userAgent.os.major; - if (userAgent.os.minor != null) { - os += "." + userAgent.os.minor; - if (userAgent.os.patch != null) { - os += "." + userAgent.os.patch; - if (userAgent.os.patchMinor != null) { - os += "." + userAgent.os.patchMinor; - } - } - } - } - } - if (userAgent.device != null) { - device = userAgent.device.family; - } - } - return new AuthorizationDetails(clientAddress, browser, os, device); - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index aa9c669ca5..da94141768 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -257,12 +257,10 @@ public class AuditLogServiceImpl implements AuditLogService { String browser = extractParameter(String.class, 1, additionalInfo); String os = extractParameter(String.class, 2, additionalInfo); String device = extractParameter(String.class, 3, additionalInfo); - String provider = extractParameter(String.class, 4, additionalInfo); actionData.put("clientAddress", clientAddress); actionData.put("browser", browser); actionData.put("os", os); actionData.put("device", device); - actionData.put("provider", provider); break; case PROVISION_SUCCESS: case PROVISION_FAILURE: From 39b138d45b13c9d256c070d396c4a06c3e6965e2 Mon Sep 17 00:00:00 2001 From: oyurov Date: Wed, 19 Oct 2022 11:55:27 +0200 Subject: [PATCH 016/226] Added audit log for provider oauth2 --- .../service/security/system/DefaultSystemSecurityService.java | 2 +- .../org/thingsboard/server/dao/audit/AuditLogServiceImpl.java | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index e7743f8218..2fe4c2d639 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -310,7 +310,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { } auditLogService.logEntityAction( user.getTenantId(), user.getCustomerId(), user.getId(), - user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device); + user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device, user.getAdditionalInfo()); } private static boolean isPositiveInteger(Integer val) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index da94141768..b935307358 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -257,10 +257,14 @@ public class AuditLogServiceImpl implements AuditLogService { String browser = extractParameter(String.class, 1, additionalInfo); String os = extractParameter(String.class, 2, additionalInfo); String device = extractParameter(String.class, 3, additionalInfo); + JsonNode provider = extractParameter(JsonNode.class, 4, additionalInfo); actionData.put("clientAddress", clientAddress); actionData.put("browser", browser); actionData.put("os", os); actionData.put("device", device); + if (provider != null && provider.has("authProviderName")) { + actionData.put("provider", provider.get("authProviderName").asText()); + } break; case PROVISION_SUCCESS: case PROVISION_FAILURE: From f8158ee6aee6430b7c5a9999c6990e7fcb148c06 Mon Sep 17 00:00:00 2001 From: oyurov Date: Wed, 19 Oct 2022 15:25:33 +0200 Subject: [PATCH 017/226] Refactor --- .../security/system/DefaultSystemSecurityService.java | 6 +++++- .../thingsboard/server/dao/audit/AuditLogServiceImpl.java | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index 2fe4c2d639..ed0e8eeb96 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -268,6 +268,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { String browser = "Unknown"; String os = "Unknown"; String device = "Unknown"; + String provider = null; if (authenticationDetails instanceof RestAuthenticationDetails) { RestAuthenticationDetails details = (RestAuthenticationDetails) authenticationDetails; clientAddress = details.getClientAddress(); @@ -308,9 +309,12 @@ public class DefaultSystemSecurityService implements SystemSecurityService { if (actionType == ActionType.LOGIN && e == null) { userService.setLastLoginTs(user.getTenantId(), user.getId()); } + if (user.getAdditionalInfo() != null && user.getAdditionalInfo().has("authProviderName")) { + provider = user.getAdditionalInfo().get("authProviderName").asText(); + } auditLogService.logEntityAction( user.getTenantId(), user.getCustomerId(), user.getId(), - user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device, user.getAdditionalInfo()); + user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device, provider); } private static boolean isPositiveInteger(Integer val) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index b935307358..c3127b5278 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -257,13 +257,13 @@ public class AuditLogServiceImpl implements AuditLogService { String browser = extractParameter(String.class, 1, additionalInfo); String os = extractParameter(String.class, 2, additionalInfo); String device = extractParameter(String.class, 3, additionalInfo); - JsonNode provider = extractParameter(JsonNode.class, 4, additionalInfo); + String provider = extractParameter(String.class, 4, additionalInfo); actionData.put("clientAddress", clientAddress); actionData.put("browser", browser); actionData.put("os", os); actionData.put("device", device); - if (provider != null && provider.has("authProviderName")) { - actionData.put("provider", provider.get("authProviderName").asText()); + if (org.springframework.util.StringUtils.hasText(provider)) { + actionData.put("provider", provider); } break; case PROVISION_SUCCESS: From 0da086ff9e3dc4cf0e5d08137823f1e40644c79e Mon Sep 17 00:00:00 2001 From: oyurov Date: Wed, 19 Oct 2022 15:26:24 +0200 Subject: [PATCH 018/226] Formatting --- .../service/security/system/DefaultSystemSecurityService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index ed0e8eeb96..ca44b584fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -232,8 +232,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { JsonNode additionalInfo = user.getAdditionalInfo(); if (additionalInfo instanceof ObjectNode && additionalInfo.has(UserServiceImpl.USER_PASSWORD_HISTORY)) { JsonNode userPasswordHistoryJson = additionalInfo.get(UserServiceImpl.USER_PASSWORD_HISTORY); - Map userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() { - }); + Map userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() {}); for (Map.Entry entry : userPasswordHistoryMap.entrySet()) { if (encoder.matches(password, entry.getValue()) && Long.parseLong(entry.getKey()) > passwordReuseFrequencyTs) { throw new DataValidationException("Password was already used for the last " + passwordPolicy.getPasswordReuseFrequencyDays() + " days"); From 87cd103ec89a16260b3d6494ead948ab899e4ac2 Mon Sep 17 00:00:00 2001 From: oyurov Date: Tue, 1 Nov 2022 11:53:09 +0100 Subject: [PATCH 019/226] Refactor --- .../auth/oauth2/Oauth2AuthenticationSuccessHandler.java | 2 +- .../security/system/DefaultSystemSecurityService.java | 9 +++++---- .../service/security/system/SystemSecurityService.java | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index d41fba4ee5..6aeba94d69 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -104,7 +104,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS clearAuthenticationAttributes(request, response); getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken()); - systemSecurityService.logLoginAction(securityUser, new RestAuthenticationDetails(request), ActionType.LOGIN, null); + systemSecurityService.logLoginAction(securityUser, new RestAuthenticationDetails(request), ActionType.LOGIN, registration.getName(), null); } catch (Exception e) { log.debug("Error occurred during processing authentication success result. " + "request [{}], response [{}], authentication [{}]", request, response, authentication, e); diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index ca44b584fa..97b433661a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -263,11 +263,15 @@ public class DefaultSystemSecurityService implements SystemSecurityService { @Override public void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e) { + logLoginAction(user, authenticationDetails, actionType, null, e); + } + + @Override + public void logLoginAction(User user, Object authenticationDetails, ActionType actionType, String provider, Exception e) { String clientAddress = "Unknown"; String browser = "Unknown"; String os = "Unknown"; String device = "Unknown"; - String provider = null; if (authenticationDetails instanceof RestAuthenticationDetails) { RestAuthenticationDetails details = (RestAuthenticationDetails) authenticationDetails; clientAddress = details.getClientAddress(); @@ -308,9 +312,6 @@ public class DefaultSystemSecurityService implements SystemSecurityService { if (actionType == ActionType.LOGIN && e == null) { userService.setLastLoginTs(user.getTenantId(), user.getId()); } - if (user.getAdditionalInfo() != null && user.getAdditionalInfo().has("authProviderName")) { - provider = user.getAdditionalInfo().get("authProviderName").asText(); - } auditLogService.logEntityAction( user.getTenantId(), user.getCustomerId(), user.getId(), user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device, provider); diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java index 6173d408c5..39d5935906 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java @@ -44,4 +44,5 @@ public interface SystemSecurityService { void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e); + void logLoginAction(User user, Object authenticationDetails, ActionType actionType, String provider, Exception e); } From 4d271ff730a2cef9aca819ab9c3ff2c6786dc70c Mon Sep 17 00:00:00 2001 From: oyurov Date: Tue, 1 Nov 2022 12:08:57 +0100 Subject: [PATCH 020/226] Refactor --- .../auth/oauth2/AbstractOAuth2ClientMapper.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java index b32e762752..ba0e4aa0b4 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java @@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.IdBased; @@ -48,7 +47,7 @@ import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; -import org.thingsboard.server.service.entitiy.TbNotificationEntityService; +import org.thingsboard.server.service.entitiy.user.TbUserService; import org.thingsboard.server.service.install.InstallScripts; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -84,7 +83,7 @@ public abstract class AbstractOAuth2ClientMapper { private InstallScripts installScripts; @Autowired - private TbNotificationEntityService notificationEntityService; + private TbUserService tbUserService; @Autowired protected TbTenantProfileCache tenantProfileCache; @@ -151,14 +150,11 @@ public abstract class AbstractOAuth2ClientMapper { user.setAdditionalInfo(additionalInfo); - user = userService.saveUser(user); + user = tbUserService.save(tenantId, customerId, user, false, null, null); if (config.isActivateUser()) { UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()); userService.activateUserCredentials(user.getTenantId(), userCredentials.getActivateToken(), passwordEncoder.encode("")); } - - notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, customerId, user.getId(), - user, user, ActionType.ADDED, true, null); } } catch (Exception e) { log.error("Can't get or create security user from oauth2 user", e); From ea80f9838e09384105899239bf22eeeb697171a8 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 1 Nov 2022 19:42:38 +0200 Subject: [PATCH 021/226] JwtSettings API added to the admin controller --- .../server/config/jwt/JwtSettingsService.java | 104 +----------- .../config/jwt/JwtSettingsServiceDefault.java | 154 ++++++++++++++++++ .../config/jwt/JwtSettingsValidator.java | 58 +++++++ .../server/controller/AdminController.java | 39 +++++ 4 files changed, 256 insertions(+), 99 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java create mode 100644 application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java index bbb1bdba8b..fb673fe50c 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java @@ -15,108 +15,14 @@ */ package org.thingsboard.server.config.jwt; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; -import org.springframework.dao.InvalidDataAccessResourceUsageException; -import org.springframework.stereotype.Service; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.AdminSettings; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.settings.AdminSettingsService; +public interface JwtSettingsService { -import javax.annotation.PostConstruct; -import javax.validation.ValidationException; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Objects; + JwtSettings getJwtSettings(); -@Service -@RequiredArgsConstructor -@Slf4j -public class JwtSettingsService { + void createJwtAdminSettings(); - static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; - static final String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; - static final String TB_ALLOW_DEFAULT_JWT_SIGNING_KEY = "TB_ALLOW_DEFAULT_JWT_SIGNING_KEY"; + JwtSettings saveJwtSettings(JwtSettings jwtSettings); - private final AdminSettingsService adminSettingsService; - - @Getter - private final JwtSettings jwtSettings; - - @PostConstruct - public void init() { - AdminSettings adminJwtSettings = findJwtAdminSettings(); - if (adminJwtSettings != null) { - log.debug("Loading the JWT admin settings from database"); - JwtSettings jwtLoaded = JacksonUtil.treeToValue(adminJwtSettings.getJsonValue(), JwtSettings.class); - jwtSettings.setRefreshTokenExpTime(jwtLoaded.getRefreshTokenExpTime()); - jwtSettings.setTokenExpirationTime(jwtLoaded.getTokenExpirationTime()); - jwtSettings.setTokenIssuer(jwtLoaded.getTokenIssuer()); - jwtSettings.setTokenSigningKey(jwtLoaded.getTokenSigningKey()); - } - - if (hasDefaultTokenSigningKey()) { - log.warn("JWT token signing key is default. This is a security issue. Please, consider to set unique value"); - } - } - - public boolean hasDefaultTokenSigningKey() { - return TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey()); - } - - public void createJwtAdminSettings() { - log.debug("Creating JWT admin settings..."); - Objects.requireNonNull(jwtSettings, "JWT settings is null"); - if (isJwtAdminSettingsNotExists()) { - if (hasDefaultTokenSigningKey()) { - if (!isAllowedDefaultJwtSigningKey()) { - log.info("JWT token signing key is default. Generating a new random key"); - jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); - } - } - AdminSettings adminJwtSettings = new AdminSettings(); - adminJwtSettings.setTenantId(TenantId.SYS_TENANT_ID); - adminJwtSettings.setKey(ADMIN_SETTINGS_JWT_KEY); - adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(jwtSettings)); - log.info("Saving new JWT admin settings. From this moment, the JWT parameters from YAML and ENV will be ignored"); - adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminJwtSettings); - } - } - - public boolean isJwtAdminSettingsNotExists() { - return findJwtAdminSettings() == null; - } - - AdminSettings findJwtAdminSettings() { - try { - return adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); - } catch (InvalidDataAccessResourceUsageException ignored) { - log.debug("findAdminSettingsByKey is returning InvalidDataAccessResourceUsageException. This is an installation case when the database is not initialized yet"); - return null; - } - } - - /* - * Allowing default JWT signing key is not secure - * */ - public boolean isAllowedDefaultJwtSigningKey() { - String allowDefaultJwtSigningKey = System.getenv(TB_ALLOW_DEFAULT_JWT_SIGNING_KEY); - return "true".equalsIgnoreCase(allowDefaultJwtSigningKey); - } - - public void validateJwtTokenSigningKey() { - if (isJwtAdminSettingsNotExists() && hasDefaultTokenSigningKey()) { - if (isAllowedDefaultJwtSigningKey()) { - log.warn("Default JWT signing key is allowed. This is a security issue. Please, consider to set a strong key in admin settings"); - } else { - String message = "Please, set a unique signing key with env variable JWT_TOKEN_SIGNING_KEY. Key is a Base64 encoded phrase. This will require to generate new tokens for all users and API that uses JWT tokens. To allow insecure JWS use TB_ALLOW_DEFAULT_JWT_SIGNING_KEY=true"; - log.error(message); - throw new ValidationException(message); - } - } - } + void validateJwtTokenSigningKey(); } diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java new file mode 100644 index 0000000000..5c172c9c2d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java @@ -0,0 +1,154 @@ +/** + * Copyright © 2016-2022 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.config.jwt; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.dao.settings.AdminSettingsService; + +import javax.annotation.PostConstruct; +import javax.validation.ValidationException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +@Slf4j +public class JwtSettingsServiceDefault implements JwtSettingsService { + + static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; + static final String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; + static final String TB_ALLOW_DEFAULT_JWT_SIGNING_KEY = "TB_ALLOW_DEFAULT_JWT_SIGNING_KEY"; + + private final AdminSettingsService adminSettingsService; + private final TbClusterService tbClusterService; + + private final JwtSettingsValidator jwtSettingsValidator; + + @Getter + private final JwtSettings jwtSettings; + + @PostConstruct + public void init() { + reloadJwtSettings(); + } + + void reloadJwtSettings() { + AdminSettings adminJwtSettings = findJwtAdminSettings(); + if (adminJwtSettings != null) { + log.debug("Loading the JWT admin settings from database"); + JwtSettings jwtLoaded = mapAdminToJwtSettings(adminJwtSettings); + jwtSettings.setRefreshTokenExpTime(jwtLoaded.getRefreshTokenExpTime()); + jwtSettings.setTokenExpirationTime(jwtLoaded.getTokenExpirationTime()); + jwtSettings.setTokenIssuer(jwtLoaded.getTokenIssuer()); + jwtSettings.setTokenSigningKey(jwtLoaded.getTokenSigningKey()); + } + + if (hasDefaultTokenSigningKey()) { + log.warn("JWT token signing key is default. This is a security issue. Please, consider to set unique value"); + } + } + + JwtSettings mapAdminToJwtSettings(AdminSettings adminSettings) { + Objects.requireNonNull(adminSettings, "adminSettings for JWT is null"); + return JacksonUtil.treeToValue(adminSettings.getJsonValue(), JwtSettings.class); + } + + AdminSettings mapJwtToAdminSettings(JwtSettings jwtSettings) { + Objects.requireNonNull(jwtSettings, "jwtSettings is null"); + AdminSettings adminJwtSettings = new AdminSettings(); + adminJwtSettings.setTenantId(TenantId.SYS_TENANT_ID); + adminJwtSettings.setKey(ADMIN_SETTINGS_JWT_KEY); + adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(jwtSettings)); + return adminJwtSettings; + } + + boolean hasDefaultTokenSigningKey() { + return TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey()); + } + + @Override + public void createJwtAdminSettings() { + log.debug("Creating JWT admin settings..."); + Objects.requireNonNull(jwtSettings, "JWT settings is null"); + if (isJwtAdminSettingsNotExists()) { + if (hasDefaultTokenSigningKey()) { + if (!isAllowedDefaultJwtSigningKey()) { + log.info("JWT token signing key is default. Generating a new random key"); + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( + RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); + } + } + saveJwtSettings(jwtSettings); + } + } + + @Override + public JwtSettings saveJwtSettings(JwtSettings jwtSettings){ + jwtSettingsValidator.validate(jwtSettings); + AdminSettings adminJwtSettings = mapJwtToAdminSettings(jwtSettings); + log.info("Saving new JWT admin settings. From this moment, the JWT parameters from YAML and ENV will be ignored"); + adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminJwtSettings); + tbClusterService.broadcastEntityStateChangeEvent(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, ComponentLifecycleEvent.UPDATED); + reloadJwtSettings(); + return getJwtSettings(); + } + + boolean isJwtAdminSettingsNotExists() { + return findJwtAdminSettings() == null; + } + + AdminSettings findJwtAdminSettings() { + try { + return adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); + } catch (InvalidDataAccessResourceUsageException ignored) { + log.debug("findAdminSettingsByKey is returning InvalidDataAccessResourceUsageException. This is an installation case when the database is not initialized yet"); + return null; + } + } + + /* + * Allowing default JWT signing key is not secure + * */ + boolean isAllowedDefaultJwtSigningKey() { + String allowDefaultJwtSigningKey = System.getenv(TB_ALLOW_DEFAULT_JWT_SIGNING_KEY); + return "true".equalsIgnoreCase(allowDefaultJwtSigningKey); + } + + @Override + public void validateJwtTokenSigningKey() { + if (isJwtAdminSettingsNotExists() && hasDefaultTokenSigningKey()) { + if (isAllowedDefaultJwtSigningKey()) { + log.warn("Default JWT signing key is allowed. This is a security issue. Please, consider to set a strong key in admin settings"); + } else { + String message = "Please, set a unique signing key with env variable JWT_TOKEN_SIGNING_KEY. Key is a Base64 encoded phrase. This will require to generate new tokens for all users and API that uses JWT tokens. To allow insecure JWS use TB_ALLOW_DEFAULT_JWT_SIGNING_KEY=true"; + log.error(message); + throw new ValidationException(message); + } + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java new file mode 100644 index 0000000000..4e91c654e0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2022 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.config.jwt; + +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.util.Arrays; +import org.springframework.stereotype.Component; +import org.thingsboard.server.dao.exception.DataValidationException; + +import java.util.Base64; +import java.util.Optional; + +@Component +@AllArgsConstructor +public class JwtSettingsValidator { + + public void validate(JwtSettings jwtSettings) { + if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) { + throw new DataValidationException("JWT token issuer should be specified!"); + } + if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) <= 0) { + throw new DataValidationException("JWT refresh token expiration time should be specified!"); + } + if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= 0) { + throw new DataValidationException("JWT token expiration time should be specified!"); + } + if (StringUtils.isEmpty(jwtSettings.getTokenSigningKey())) { + throw new DataValidationException("JWT token signing key should be specified!"); + } + + byte[] decodedKey; + try { + decodedKey = Base64.getDecoder().decode(jwtSettings.getTokenSigningKey()); + } catch (Exception e) { + throw new DataValidationException("JWT token signing key should be valid Base64 encoded string! " + e.getCause()); + } + + if (Arrays.isNullOrEmpty(decodedKey)) { + throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!"); + } + Arrays.fill(decodedKey, (byte) 0); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 54cbdd9b0f..09f552d0fd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -23,6 +23,7 @@ import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import org.springframework.web.context.request.async.DeferredResult; @@ -36,6 +37,8 @@ import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.sms.config.TestSmsRequest; import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings; +import org.thingsboard.server.config.jwt.JwtSettings; +import org.thingsboard.server.config.jwt.JwtSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; @@ -64,6 +67,9 @@ public class AdminController extends BaseController { @Autowired private SystemSecurityService systemSecurityService; + @Autowired + private JwtSettingsService jwtSettingsService; + @Autowired private EntitiesVersionControlService versionControlService; @@ -151,6 +157,39 @@ public class AdminController extends BaseController { } } + @ApiOperation(value = "Get the JWT Settings object (getJwtSettings)", + notes = "Get the JWT Settings object that contains JWT token policy, etc. " + SYSTEM_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/jwtSettings", method = RequestMethod.GET) + @ResponseBody + public JwtSettings getJwtSettings() throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); + return checkNotNull(jwtSettingsService.getJwtSettings()); + } catch (Exception e) { + throw handleException(e); + } + } + + @ApiOperation(value = "Update JWT Settings (saveSecuritySettings)", + notes = "Updates the JWT Settings object that contains JWT token policy, etc. The tokenSigningKey field is a Base64 encoded string." + SYSTEM_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/jwtSettings", method = RequestMethod.POST) + @ResponseBody + public JwtSettings saveJwtSettings( + @ApiParam(value = "A JSON value representing the JWT Settings.") + @RequestBody JwtSettings jwtSettings) throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); + jwtSettings = checkNotNull(jwtSettingsService.saveJwtSettings(jwtSettings)); + return jwtSettings; + } catch (Exception e) { + throw handleException(e); + } + } + @ApiOperation(value = "Send test email (sendTestMail)", notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " + "You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH) From a08c716ad28a01d2e226aaa7002e4818182162aa Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 1 Nov 2022 20:11:41 +0200 Subject: [PATCH 022/226] upgrade for JwtSetting targeted from 3.4.1 to 3.4.2 --- .../server/install/ThingsboardInstallService.java | 4 ---- .../service/install/update/DefaultDataUpdateService.java | 5 +---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 52de6c91fb..10f6d38430 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -238,10 +238,6 @@ public class ThingsboardInstallService { log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); break; - case "3.4.0": - log.info("Upgrading ThingsBoard from version 3.4.0 to 3.5.0 ..."); - dataUpdateService.updateData("3.4.0"); - break; //TODO update CacheCleanupService on the next version upgrade diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index 4909df84e8..d00d8f2768 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -181,6 +181,7 @@ public class DefaultDataUpdateService implements DataUpdateService { } break; case "3.4.1": + systemDataLoaderService.createJwtAdminSettings(); boolean skipAuditLogsMigration = getEnv("TB_SKIP_AUDIT_LOGS_MIGRATION", false); if (!skipAuditLogsMigration) { log.info("Updating data from version 3.4.1 to 3.4.2 ..."); @@ -190,10 +191,6 @@ public class DefaultDataUpdateService implements DataUpdateService { log.info("Skipping audit logs migration"); } break; - case "3.4.0": - log.info("Updating data from version 3.4.0 to 3.5.0 ..."); - systemDataLoaderService.createJwtAdminSettings(); - break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); } From d10289253f1bfdb62feabdf3aaee130993ee58bb Mon Sep 17 00:00:00 2001 From: oyurov Date: Wed, 2 Nov 2022 08:46:58 +0100 Subject: [PATCH 023/226] Refactor --- .../server/controller/AuthController.java | 18 +--- .../server/controller/UserController.java | 9 +- .../security/auth/TokenOutdatingService.java | 54 ++++++---- .../auth/jwt/RefreshTokenRepository.java | 38 ------- .../Oauth2AuthenticationSuccessHandler.java | 11 +- ...RestAwareAuthenticationSuccessHandler.java | 5 +- .../service/security/model/SecurityUser.java | 10 ++ .../security/model/token/JwtTokenFactory.java | 6 ++ .../security/auth/TokenOutdatingTest.java | 101 +++++++++--------- .../UserUpdateTimeRedisCache.java | 39 +++++++ .../UsersUpdateTimeCacheEvictEvent.java | 25 +++++ .../UsersUpdateTimeCaffeineCache.java | 37 +++++++ .../event/UserAuthDataChangedEvent.java | 10 +- .../server/dao/user/UserServiceImpl.java | 2 +- 14 files changed, 220 insertions(+), 145 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index b3d9cb6634..84e8f3df9d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -43,12 +43,10 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; -import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.ActivateUserRequest; import org.thingsboard.server.service.security.model.ChangePasswordRequest; @@ -73,7 +71,6 @@ import java.net.URISyntaxException; public class AuthController extends BaseController { private final BCryptPasswordEncoder passwordEncoder; private final JwtTokenFactory tokenFactory; - private final RefreshTokenRepository refreshTokenRepository; private final MailService mailService; private final SystemSecurityService systemSecurityService; private final AuditLogService auditLogService; @@ -128,7 +125,7 @@ public class AuthController extends BaseController { sendEntityNotificationMsg(getTenantId(), userCredentials.getUserId(), EdgeEventActionType.CREDENTIALS_UPDATED); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId())); + eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); ObjectNode response = JacksonUtil.newObjectNode(); response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken()); response.put("refreshToken", tokenFactory.createRefreshToken(securityUser).getToken()); @@ -268,10 +265,7 @@ public class AuthController extends BaseController { sendEntityNotificationMsg(user.getTenantId(), user.getId(), EdgeEventActionType.CREDENTIALS_UPDATED); - JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); - JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); - - return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken()); + return tokenFactory.createTokenPair(securityUser); } catch (Exception e) { throw handleException(e); } @@ -309,11 +303,9 @@ public class AuthController extends BaseController { String email = user.getEmail(); mailService.sendPasswordWasResetEmail(loginUrl, email); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId())); - JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); - JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); + eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); - return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken()); + return tokenFactory.createTokenPair(securityUser); } else { throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } @@ -367,7 +359,7 @@ public class AuthController extends BaseController { user.getTenantId(), user.getCustomerId(), user.getId(), user.getName(), user.getId(), null, ActionType.LOGOUT, null, clientAddress, browser, os, device); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(user.getId())); + eventPublisher.publishEvent(new UserAuthDataChangedEvent(user.getId(), user.getSessionId(), false)); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index e96a73b65c..6f19cde2e8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -44,10 +44,8 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; -import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.user.TbUserService; -import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -95,7 +93,6 @@ public class UserController extends BaseController { private final MailService mailService; private final JwtTokenFactory tokenFactory; - private final RefreshTokenRepository refreshTokenRepository; private final SystemSecurityService systemSecurityService; private final ApplicationEventPublisher eventPublisher; private final TbUserService tbUserService; @@ -163,9 +160,7 @@ public class UserController extends BaseController { UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); UserCredentials credentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), userId); SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal); - JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); - JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); - return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken()); + return tokenFactory.createTokenPair(securityUser); } catch (Exception e) { throw handleException(e); } @@ -376,7 +371,7 @@ public class UserController extends BaseController { userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled); if (!userCredentialsEnabled) { - eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId)); + eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId, null, true)); } } catch (Exception e) { throw handleException(e); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java index a623fc6862..6bb74ed758 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java @@ -17,18 +17,18 @@ package org.thingsboard.server.service.security.auth; import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.CacheConstants; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.server.cache.usersUpdateTime.UsersUpdateTimeCacheEvictEvent; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.config.JwtSettings; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; -import javax.annotation.PostConstruct; +import java.util.HashMap; import java.util.Optional; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -36,30 +36,24 @@ import static java.util.concurrent.TimeUnit.SECONDS; @Service @RequiredArgsConstructor -public class TokenOutdatingService { - private final CacheManager cacheManager; +public class TokenOutdatingService extends AbstractCachedEntityService, UsersUpdateTimeCacheEvictEvent> { private final JwtTokenFactory tokenFactory; private final JwtSettings jwtSettings; - private Cache usersUpdateTimeCache; - - @PostConstruct - protected void initCache() { - usersUpdateTimeCache = cacheManager.getCache(CacheConstants.USERS_UPDATE_TIME_CACHE); - } @EventListener(classes = UserAuthDataChangedEvent.class) public void onUserAuthDataChanged(UserAuthDataChangedEvent event) { - usersUpdateTimeCache.put(toKey(event.getUserId()), event.getTs()); + processUserSessions(event); } public boolean isOutdated(JwtToken token, UserId userId) { Claims claims = tokenFactory.parseTokenClaims(token).getBody(); long issueTime = claims.getIssuedAt().getTime(); - return Optional.ofNullable(usersUpdateTimeCache.get(toKey(userId), Long.class)) + String sessionId = claims.get("sessionId", String.class); + return Optional.ofNullable(cache.get(userId)) .map(outdatageTime -> { - if (System.currentTimeMillis() - outdatageTime <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime())) { - return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); + if (outdatageTime.get().get(sessionId) != null && System.currentTimeMillis() - outdatageTime.get().get(sessionId) <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime())) { + return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime.get().get(sessionId)); } else { /* * Means that since the outdating has passed more than @@ -68,14 +62,36 @@ public class TokenOutdatingService { * as all the tokens issued before the outdatage time * are now expired by themselves * */ - usersUpdateTimeCache.evict(toKey(userId)); + handleEvictEvent(new UsersUpdateTimeCacheEvictEvent(userId, sessionId)); return false; } }) .orElse(false); } - private String toKey(UserId userId) { - return userId.getId().toString(); + @TransactionalEventListener(classes = UsersUpdateTimeCacheEvictEvent.class) + @Override + public void handleEvictEvent(UsersUpdateTimeCacheEvictEvent event) { + HashMap userSessions = cache.get(event.getUserId()).get(); + if (userSessions != null) { + userSessions.remove(event.getSessionId()); + cache.put(event.getUserId(), userSessions); + } + } + + private void processUserSessions(UserAuthDataChangedEvent event) { + if (cache.get(event.getUserId()) != null) { + HashMap userSessions = cache.get(event.getUserId()).get(); + if (event.isDropAllSessions()) { + userSessions.replaceAll((k, v) -> event.getTs()); + } else { + userSessions.put(event.getSessionId(), event.getTs()); + } + cache.put(event.getUserId(), userSessions); + } else { + cache.put(event.getUserId(), new HashMap<>() {{ + put(event.getSessionId(), event.getTs()); + }}); + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java deleted file mode 100644 index c5a666d464..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright © 2016-2022 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.security.auth.jwt; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.service.security.model.SecurityUser; -import org.thingsboard.server.service.security.model.token.JwtTokenFactory; - -@Component -public class RefreshTokenRepository { - - private final JwtTokenFactory tokenFactory; - - @Autowired - public RefreshTokenRepository(final JwtTokenFactory tokenFactory) { - this.tokenFactory = tokenFactory; - } - - public JwtToken requestRefreshToken(SecurityUser user) { - return tokenFactory.createRefreshToken(user); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index e2a78eb605..7b85f22c2d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -29,10 +29,9 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.OAuth2Registration; -import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; +import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.system.SystemSecurityService; @@ -50,7 +49,6 @@ import java.util.UUID; public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final JwtTokenFactory tokenFactory; - private final RefreshTokenRepository refreshTokenRepository; private final OAuth2ClientMapperProvider oauth2ClientMapperProvider; private final OAuth2Service oAuth2Service; private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService; @@ -59,14 +57,12 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS @Autowired public Oauth2AuthenticationSuccessHandler(final JwtTokenFactory tokenFactory, - final RefreshTokenRepository refreshTokenRepository, final OAuth2ClientMapperProvider oauth2ClientMapperProvider, final OAuth2Service oAuth2Service, final OAuth2AuthorizedClientService oAuth2AuthorizedClientService, final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository, final SystemSecurityService systemSecurityService) { this.tokenFactory = tokenFactory; - this.refreshTokenRepository = refreshTokenRepository; this.oauth2ClientMapperProvider = oauth2ClientMapperProvider; this.oAuth2Service = oAuth2Service; this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService; @@ -97,11 +93,10 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), registration); - JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); - JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); + JwtTokenPair tokenPair = tokenFactory.createTokenPair(securityUser); clearAuthenticationAttributes(request, response); - getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken()); + getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken()); } catch (Exception e) { log.debug("Error occurred during processing authentication success result. " + "request [{}], response [{}], authentication [{}]", request, response, authentication, e); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java index b4f0b293d3..4d7ef01914 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java @@ -25,7 +25,6 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.auth.MfaAuthenticationToken; -import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; @@ -45,7 +44,6 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc private final ObjectMapper mapper; private final JwtTokenFactory tokenFactory; private final TwoFaConfigManager twoFaConfigManager; - private final RefreshTokenRepository refreshTokenRepository; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, @@ -62,8 +60,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc tokenPair.setRefreshToken(null); tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN); } else { - tokenPair.setToken(tokenFactory.createAccessJwtToken(securityUser).getToken()); - tokenPair.setRefreshToken(refreshTokenRepository.requestRefreshToken(securityUser).getToken()); + tokenPair = tokenFactory.createTokenPair(securityUser); } response.setStatus(HttpStatus.OK.value()); diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java index 380d6537f2..b7f480dfab 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.UserId; import java.util.Collection; +import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -31,6 +32,7 @@ public class SecurityUser extends User { private Collection authorities; private boolean enabled; private UserPrincipal userPrincipal; + private String sessionId; public SecurityUser() { super(); @@ -44,6 +46,7 @@ public class SecurityUser extends User { super(user); this.enabled = enabled; this.userPrincipal = userPrincipal; + this.sessionId = UUID.randomUUID().toString(); } public Collection getAuthorities() { @@ -71,4 +74,11 @@ public class SecurityUser extends User { this.userPrincipal = userPrincipal; } + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 8200f0c3d3..110ed5702b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -60,6 +60,7 @@ public class JwtTokenFactory { private static final String IS_PUBLIC = "isPublic"; private static final String TENANT_ID = "tenantId"; private static final String CUSTOMER_ID = "customerId"; + private static final String SESSION_ID = "sessionId"; private final JwtSettings settings; @@ -119,6 +120,7 @@ public class JwtTokenFactory { if (customerId != null) { securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId))); } + securityUser.setSessionId(claims.get(SESSION_ID, String.class)); UserPrincipal principal; if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) { @@ -161,6 +163,7 @@ public class JwtTokenFactory { UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); securityUser.setUserPrincipal(principal); + securityUser.setSessionId(claims.get(SESSION_ID, String.class)); return securityUser; } @@ -183,6 +186,9 @@ public class JwtTokenFactory { Claims claims = Jwts.claims().setSubject(principal.getValue()); claims.put(USER_ID, securityUser.getId().getId().toString()); claims.put(SCOPES, scopes); + if (securityUser.getSessionId() != null) { + claims.put(SESSION_ID, securityUser.getSessionId()); + } ZonedDateTime currentTime = ZonedDateTime.now(); diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java index f804d4dcfd..f0e758be14 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java @@ -15,19 +15,27 @@ */ package org.thingsboard.server.service.security.auth; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootContextLoader; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.ComponentScan; import org.springframework.security.authentication.CredentialsExpiredException; -import org.thingsboard.server.common.data.CacheConstants; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; import org.thingsboard.server.common.data.User; 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.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.config.JwtSettings; import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider; @@ -39,13 +47,9 @@ import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; import java.util.UUID; -import static java.util.concurrent.TimeUnit.DAYS; -import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -53,31 +57,33 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@ActiveProfiles("test") +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = TokenOutdatingTest.class, loader = SpringBootContextLoader.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +@ComponentScan({"org.thingsboard.server"}) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DaoSqlTest +@TestPropertySource(properties = { + "security.jwt.tokenIssuer=test.io", + "security.jwt.tokenSigningKey=secret", + "security.jwt.tokenExpirationTime=600", + "security.jwt.refreshTokenExpTime=10" +}) public class TokenOutdatingTest { private JwtAuthenticationProvider accessTokenAuthenticationProvider; private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider; + @Autowired private TokenOutdatingService tokenOutdatingService; - private ConcurrentMapCacheManager cacheManager; + @Autowired private JwtTokenFactory tokenFactory; - private JwtSettings jwtSettings; + private SecurityUser securityUser; - private UserId userId; - - @BeforeEach + @Before public void setUp() { - jwtSettings = new JwtSettings(); - jwtSettings.setTokenIssuer("test.io"); - jwtSettings.setTokenExpirationTime((int) MINUTES.toSeconds(10)); - jwtSettings.setRefreshTokenExpTime((int) DAYS.toSeconds(7)); - jwtSettings.setTokenSigningKey("secret"); - tokenFactory = new JwtTokenFactory(jwtSettings); - - cacheManager = new ConcurrentMapCacheManager(); - tokenOutdatingService = new TokenOutdatingService(cacheManager, tokenFactory, jwtSettings); - tokenOutdatingService.initCache(); - - userId = new UserId(UUID.randomUUID()); + UserId userId = new UserId(UUID.randomUUID()); + securityUser = createMockSecurityUser(userId); UserService userService = mock(UserService.class); @@ -97,28 +103,28 @@ public class TokenOutdatingTest { @Test public void testOutdateOldUserTokens() throws Exception { - JwtToken jwtToken = createAccessJwtToken(userId); + JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser); SECONDS.sleep(1); // need to wait before outdating so that outdatage time is strictly after token issue time - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId)); - assertTrue(tokenOutdatingService.isOutdated(jwtToken, userId)); + tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); SECONDS.sleep(1); - JwtToken newJwtToken = tokenFactory.createAccessJwtToken(createMockSecurityUser(userId)); - assertFalse(tokenOutdatingService.isOutdated(newJwtToken, userId)); + JwtToken newJwtToken = tokenFactory.createAccessJwtToken(securityUser); + assertFalse(tokenOutdatingService.isOutdated(newJwtToken, securityUser.getId())); } @Test public void testAuthenticateWithOutdatedAccessToken() throws InterruptedException { - RawAccessJwtToken accessJwtToken = getRawJwtToken(createAccessJwtToken(userId)); + RawAccessJwtToken accessJwtToken = getRawJwtToken(tokenFactory.createAccessJwtToken(securityUser)); assertDoesNotThrow(() -> { accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken)); }); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId)); + tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); assertThrows(JwtExpiredTokenException.class, () -> { accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken)); @@ -127,14 +133,14 @@ public class TokenOutdatingTest { @Test public void testAuthenticateWithOutdatedRefreshToken() throws InterruptedException { - RawAccessJwtToken refreshJwtToken = getRawJwtToken(createRefreshJwtToken(userId)); + RawAccessJwtToken refreshJwtToken = getRawJwtToken(tokenFactory.createRefreshToken(securityUser)); assertDoesNotThrow(() -> { refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken)); }); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId)); + tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); assertThrows(CredentialsExpiredException.class, () -> { refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken)); @@ -143,32 +149,20 @@ public class TokenOutdatingTest { @Test public void testTokensOutdatageTimeRemovalFromCache() throws Exception { - JwtToken jwtToken = createAccessJwtToken(userId); + JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId)); - - int refreshTokenExpirationTime = 3; - jwtSettings.setRefreshTokenExpTime(refreshTokenExpirationTime); + tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); - SECONDS.sleep(refreshTokenExpirationTime - 2); - - assertTrue(tokenOutdatingService.isOutdated(jwtToken, userId)); - assertNotNull(cacheManager.getCache(CacheConstants.USERS_UPDATE_TIME_CACHE).get(userId.getId().toString())); + SECONDS.sleep(1); - SECONDS.sleep(3); + assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); - assertFalse(tokenOutdatingService.isOutdated(jwtToken, userId)); - assertNull(cacheManager.getCache(CacheConstants.USERS_UPDATE_TIME_CACHE).get(userId.getId().toString())); - } + SECONDS.sleep(10); - private JwtToken createAccessJwtToken(UserId userId) { - return tokenFactory.createAccessJwtToken(createMockSecurityUser(userId)); + assertFalse(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); } - private JwtToken createRefreshJwtToken(UserId userId) { - return tokenFactory.createRefreshToken(createMockSecurityUser(userId)); - } private RawAccessJwtToken getRawJwtToken(JwtToken token) { return new RawAccessJwtToken(token.getToken()); @@ -180,6 +174,7 @@ public class TokenOutdatingTest { securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail())); securityUser.setAuthority(Authority.CUSTOMER_USER); securityUser.setId(userId); + securityUser.setSessionId(UUID.randomUUID().toString()); return securityUser; } } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java new file mode 100644 index 0000000000..3d96d4c2a7 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cache.usersUpdateTime; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbFSTRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.UserId; + +import java.util.HashMap; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("UsersUpdateTimeCache") +public class UserUpdateTimeRedisCache extends RedisTbTransactionalCache> { + + @Autowired + public UserUpdateTimeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.USERS_UPDATE_TIME_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>()); + } +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java new file mode 100644 index 0000000000..16173e388f --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cache.usersUpdateTime; + +import lombok.Data; +import org.thingsboard.server.common.data.id.UserId; + +@Data +public class UsersUpdateTimeCacheEvictEvent { + private final UserId userId; + private final String sessionId; +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java new file mode 100644 index 0000000000..adb2117368 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cache.usersUpdateTime; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.UserId; + +import java.util.HashMap; + + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("UsersUpdateTimeCache") +public class UsersUpdateTimeCaffeineCache extends CaffeineTbTransactionalCache> { + + @Autowired + public UsersUpdateTimeCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.USERS_UPDATE_TIME_CACHE); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java index 2dcd95f435..5d9ef3a1cd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java @@ -18,13 +18,19 @@ package org.thingsboard.server.common.data.security.event; import lombok.Data; import org.thingsboard.server.common.data.id.UserId; +import java.io.Serializable; + @Data -public class UserAuthDataChangedEvent { +public class UserAuthDataChangedEvent implements Serializable { private final UserId userId; + private final String sessionId; private final long ts; + private final boolean dropAllSessions; - public UserAuthDataChangedEvent(UserId userId) { + public UserAuthDataChangedEvent(UserId userId, String sessionId, boolean dropAllSessions) { this.userId = userId; + this.sessionId = sessionId; + this.dropAllSessions = dropAllSessions; this.ts = System.currentTimeMillis(); } 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 fc0de8a4fa..78ad93b68c 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 @@ -211,7 +211,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic userAuthSettingsDao.removeByUserId(userId); deleteEntityRelations(tenantId, userId); userDao.removeById(tenantId, userId.getId()); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId)); + eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId, null, true)); } @Override From 215a44b7c7256a87c90858664b962f2cb5571ddd Mon Sep 17 00:00:00 2001 From: oyurov Date: Wed, 2 Nov 2022 09:19:51 +0100 Subject: [PATCH 024/226] Added tests --- .../security/auth/TokenOutdatingTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java index f0e758be14..187b77fd75 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java @@ -163,6 +163,29 @@ public class TokenOutdatingTest { assertFalse(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); } + @Test + public void testOnlyOneTokenExpired() throws InterruptedException { + JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser); + + SecurityUser anotherSecurityUser = new SecurityUser(securityUser, securityUser.isEnabled(), securityUser.getUserPrincipal()); + JwtToken anotherJwtToken = tokenFactory.createAccessJwtToken(anotherSecurityUser); + + assertDoesNotThrow(() -> { + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken))); + }); + + SECONDS.sleep(1); + tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + + assertThrows(JwtExpiredTokenException.class, () -> { + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken))); + }); + + assertDoesNotThrow(() -> { + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(anotherJwtToken))); + }); + } + private RawAccessJwtToken getRawJwtToken(JwtToken token) { return new RawAccessJwtToken(token.getToken()); From 6d34aa237c0e0e11e8ac7bfc8e0e5f2bcc6a9578 Mon Sep 17 00:00:00 2001 From: oyurov Date: Wed, 2 Nov 2022 13:03:17 +0100 Subject: [PATCH 025/226] Refactoring --- .../server/controller/AuthController.java | 8 ++- .../server/controller/UserController.java | 3 +- .../security/auth/TokenOutdatingService.java | 67 ++++++++----------- .../RefreshTokenAuthenticationProvider.java | 2 +- .../security/model/token/JwtTokenFactory.java | 8 ++- .../security/auth/TokenOutdatingTest.java | 41 ++++++++++-- .../UserUpdateTimeRedisCache.java | 5 +- .../UsersUpdateTimeCacheEvictEvent.java | 4 +- .../UsersUpdateTimeCaffeineCache.java | 5 +- .../event/UserAuthDataChangedEvent.java | 17 +---- .../UserCredentialsInvalidationEvent.java | 42 ++++++++++++ .../event/UserSessionInvalidationEvent.java | 39 +++++++++++ .../server/dao/user/UserServiceImpl.java | 3 +- 13 files changed, 166 insertions(+), 78 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserSessionInvalidationEvent.java diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 84e8f3df9d..d098b1edec 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -43,6 +43,8 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; +import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; +import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.dao.audit.AuditLogService; @@ -125,7 +127,7 @@ public class AuthController extends BaseController { sendEntityNotificationMsg(getTenantId(), userCredentials.getUserId(), EdgeEventActionType.CREDENTIALS_UPDATED); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); ObjectNode response = JacksonUtil.newObjectNode(); response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken()); response.put("refreshToken", tokenFactory.createRefreshToken(securityUser).getToken()); @@ -303,7 +305,7 @@ public class AuthController extends BaseController { String email = user.getEmail(); mailService.sendPasswordWasResetEmail(loginUrl, email); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); return tokenFactory.createTokenPair(securityUser); } else { @@ -359,7 +361,7 @@ public class AuthController extends BaseController { user.getTenantId(), user.getCustomerId(), user.getId(), user.getName(), user.getId(), null, ActionType.LOGOUT, null, clientAddress, browser, os, device); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(user.getId(), user.getSessionId(), false)); + eventPublisher.publishEvent(new UserSessionInvalidationEvent(user.getSessionId())); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index 6f19cde2e8..fe07c4b49c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; +import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.user.TbUserService; import org.thingsboard.server.service.security.model.JwtTokenPair; @@ -371,7 +372,7 @@ public class UserController extends BaseController { userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled); if (!userCredentialsEnabled) { - eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId, null, true)); + eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId)); } } catch (Exception e) { throw handleException(e); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java index 6bb74ed758..d5f62037af 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java @@ -19,16 +19,14 @@ import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; -import org.springframework.transaction.event.TransactionalEventListener; -import org.thingsboard.server.cache.usersUpdateTime.UsersUpdateTimeCacheEvictEvent; +import org.thingsboard.server.cache.TbCacheValueWrapper; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.config.JwtSettings; -import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; -import java.util.HashMap; import java.util.Optional; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -36,13 +34,14 @@ import static java.util.concurrent.TimeUnit.SECONDS; @Service @RequiredArgsConstructor -public class TokenOutdatingService extends AbstractCachedEntityService, UsersUpdateTimeCacheEvictEvent> { +public class TokenOutdatingService { + private final TbTransactionalCache cache; private final JwtTokenFactory tokenFactory; private final JwtSettings jwtSettings; @EventListener(classes = UserAuthDataChangedEvent.class) public void onUserAuthDataChanged(UserAuthDataChangedEvent event) { - processUserSessions(event); + cache.put(event.getId(), event.getTs()); } public boolean isOutdated(JwtToken token, UserId userId) { @@ -50,48 +49,36 @@ public class TokenOutdatingService extends AbstractCachedEntityService { - if (outdatageTime.get().get(sessionId) != null && System.currentTimeMillis() - outdatageTime.get().get(sessionId) <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime())) { - return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime.get().get(sessionId)); + + Boolean isUserIdOutdated = Optional.ofNullable(cache.get(userId.toString())) + .map(outdatageTimeByUserId -> { + if (refreshTokenNotExpired(outdatageTimeByUserId.get(), System.currentTimeMillis())) { + return accessTokenNotExpired(issueTime, outdatageTimeByUserId.get()); } else { - /* - * Means that since the outdating has passed more than - * the lifetime of refresh token (the longest lived) - * and there is no need to store outdatage time anymore - * as all the tokens issued before the outdatage time - * are now expired by themselves - * */ - handleEvictEvent(new UsersUpdateTimeCacheEvictEvent(userId, sessionId)); return false; } }) .orElse(false); - } - @TransactionalEventListener(classes = UsersUpdateTimeCacheEvictEvent.class) - @Override - public void handleEvictEvent(UsersUpdateTimeCacheEvictEvent event) { - HashMap userSessions = cache.get(event.getUserId()).get(); - if (userSessions != null) { - userSessions.remove(event.getSessionId()); - cache.put(event.getUserId(), userSessions); + if (!isUserIdOutdated) { + return Optional.ofNullable(cache.get(sessionId)).map(outdatageTimeBySessionId -> { + if (refreshTokenNotExpired(outdatageTimeBySessionId.get(), System.currentTimeMillis())) { + return accessTokenNotExpired(issueTime, outdatageTimeBySessionId.get()); + } else { + return false; + } + } + ).orElse(false); } + + return isUserIdOutdated; } - private void processUserSessions(UserAuthDataChangedEvent event) { - if (cache.get(event.getUserId()) != null) { - HashMap userSessions = cache.get(event.getUserId()).get(); - if (event.isDropAllSessions()) { - userSessions.replaceAll((k, v) -> event.getTs()); - } else { - userSessions.put(event.getSessionId(), event.getTs()); - } - cache.put(event.getUserId(), userSessions); - } else { - cache.put(event.getUserId(), new HashMap<>() {{ - put(event.getSessionId(), event.getTs()); - }}); - } + private boolean accessTokenNotExpired(long issueTime, Long outdatageTime) { + return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); + } + + private boolean refreshTokenNotExpired(Long outdatageTime, long currentTime) { + return currentTime - outdatageTime <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index 8003cfd012..700bbd7f74 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -66,7 +66,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide } else { securityUser = authenticateByPublicId(principal.getValue()); } - + securityUser.setSessionId(unsafeUser.getSessionId()); if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) { throw new CredentialsExpiredException("Token is outdated"); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 110ed5702b..dca366df18 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -120,7 +120,9 @@ public class JwtTokenFactory { if (customerId != null) { securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId))); } - securityUser.setSessionId(claims.get(SESSION_ID, String.class)); + if (claims.get(SESSION_ID, String.class) != null) { + securityUser.setSessionId(claims.get(SESSION_ID, String.class)); + } UserPrincipal principal; if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) { @@ -163,7 +165,9 @@ public class JwtTokenFactory { UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); securityUser.setUserPrincipal(principal); - securityUser.setSessionId(claims.get(SESSION_ID, String.class)); + if (claims.get(SESSION_ID, String.class) != null) { + securityUser.setSessionId(claims.get(SESSION_ID, String.class)); + } return securityUser; } diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java index 187b77fd75..52371e1755 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java @@ -33,6 +33,8 @@ 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.common.data.security.event.UserAuthDataChangedEvent; +import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; +import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -106,7 +108,7 @@ public class TokenOutdatingTest { JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser); SECONDS.sleep(1); // need to wait before outdating so that outdatage time is strictly after token issue time - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId())); assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); SECONDS.sleep(1); @@ -124,7 +126,7 @@ public class TokenOutdatingTest { }); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId())); assertThrows(JwtExpiredTokenException.class, () -> { accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken)); @@ -140,7 +142,7 @@ public class TokenOutdatingTest { }); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId())); assertThrows(CredentialsExpiredException.class, () -> { refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken)); @@ -152,7 +154,7 @@ public class TokenOutdatingTest { JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId())); SECONDS.sleep(1); @@ -175,7 +177,8 @@ public class TokenOutdatingTest { }); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + + tokenOutdatingService.onUserAuthDataChanged(new UserSessionInvalidationEvent(securityUser.getSessionId())); assertThrows(JwtExpiredTokenException.class, () -> { accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken))); @@ -186,6 +189,34 @@ public class TokenOutdatingTest { }); } + @Test + public void testResetAllSessions() throws InterruptedException { + JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser); + + SecurityUser anotherSecurityUser = new SecurityUser(securityUser, securityUser.isEnabled(), securityUser.getUserPrincipal()); + JwtToken anotherJwtToken = tokenFactory.createAccessJwtToken(anotherSecurityUser); + + assertDoesNotThrow(() -> { + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken))); + }); + + assertDoesNotThrow(() -> { + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(anotherJwtToken))); + }); + + SECONDS.sleep(1); + + tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId())); + + assertThrows(JwtExpiredTokenException.class, () -> { + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken))); + }); + + assertThrows(JwtExpiredTokenException.class, () -> { + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(anotherJwtToken))); + }); + } + private RawAccessJwtToken getRawJwtToken(JwtToken token) { return new RawAccessJwtToken(token.getToken()); diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java index 3d96d4c2a7..c4084773a2 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java @@ -24,13 +24,10 @@ import org.thingsboard.server.cache.RedisTbTransactionalCache; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.cache.TbFSTRedisSerializer; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.id.UserId; - -import java.util.HashMap; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("UsersUpdateTimeCache") -public class UserUpdateTimeRedisCache extends RedisTbTransactionalCache> { +public class UserUpdateTimeRedisCache extends RedisTbTransactionalCache { @Autowired public UserUpdateTimeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java index 16173e388f..1b3d980212 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java @@ -16,10 +16,8 @@ package org.thingsboard.server.cache.usersUpdateTime; import lombok.Data; -import org.thingsboard.server.common.data.id.UserId; @Data public class UsersUpdateTimeCacheEvictEvent { - private final UserId userId; - private final String sessionId; + private final String key; } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java index adb2117368..a2c07ecaf0 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java @@ -21,14 +21,11 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CaffeineTbTransactionalCache; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.id.UserId; - -import java.util.HashMap; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("UsersUpdateTimeCache") -public class UsersUpdateTimeCaffeineCache extends CaffeineTbTransactionalCache> { +public class UsersUpdateTimeCaffeineCache extends CaffeineTbTransactionalCache { @Autowired public UsersUpdateTimeCaffeineCache(CacheManager cacheManager) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java index 5d9ef3a1cd..9ceaa2386e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java @@ -20,18 +20,7 @@ import org.thingsboard.server.common.data.id.UserId; import java.io.Serializable; -@Data -public class UserAuthDataChangedEvent implements Serializable { - private final UserId userId; - private final String sessionId; - private final long ts; - private final boolean dropAllSessions; - - public UserAuthDataChangedEvent(UserId userId, String sessionId, boolean dropAllSessions) { - this.userId = userId; - this.sessionId = sessionId; - this.dropAllSessions = dropAllSessions; - this.ts = System.currentTimeMillis(); - } - +public abstract class UserAuthDataChangedEvent implements Serializable { + public abstract String getId(); + public abstract long getTs(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java new file mode 100644 index 0000000000..26eac46fc9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.security.event; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.thingsboard.server.common.data.id.UserId; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class UserCredentialsInvalidationEvent extends UserAuthDataChangedEvent { + private final UserId userId; + private final long ts; + + public UserCredentialsInvalidationEvent(UserId userId) { + this.userId = userId; + this.ts = System.currentTimeMillis(); + } + + @Override + public String getId() { + return userId.toString(); + } + + @Override + public long getTs() { + return ts; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserSessionInvalidationEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserSessionInvalidationEvent.java new file mode 100644 index 0000000000..2e6aea6bb9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserSessionInvalidationEvent.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.security.event; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public class UserSessionInvalidationEvent extends UserAuthDataChangedEvent { + private final String sessionId; + private final long ts; + + public UserSessionInvalidationEvent(String sessionId) { + this.sessionId = sessionId; + this.ts = System.currentTimeMillis(); + } + + @Override + public String getId() { + return sessionId; + } + + @Override + public long getTs() { + return ts; + } +} 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 78ad93b68c..42aac0f59e 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 @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; +import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.DataValidator; @@ -211,7 +212,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic userAuthSettingsDao.removeByUserId(userId); deleteEntityRelations(tenantId, userId); userDao.removeById(tenantId, userId.getId()); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId, null, true)); + eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId)); } @Override From 3ab1f34594fc4595134523ac7e50e4ab9b56cbad Mon Sep 17 00:00:00 2001 From: oyurov Date: Wed, 2 Nov 2022 14:07:49 +0100 Subject: [PATCH 026/226] Clean code --- .../security/auth/TokenOutdatingService.java | 44 +++++-------------- .../src/main/resources/thingsboard.yml | 3 +- .../security/auth/TokenOutdatingTest.java | 6 +-- ...sersSessionInvalidationCaffeineCache.java} | 8 ++-- ...> UsersSessionInvalidationRedisCache.java} | 8 ++-- .../UsersUpdateTimeCacheEvictEvent.java | 23 ---------- .../server/common/data/CacheConstants.java | 2 +- .../event/UserAuthDataChangedEvent.java | 3 -- 8 files changed, 24 insertions(+), 73 deletions(-) rename common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/{UsersUpdateTimeCaffeineCache.java => UsersSessionInvalidationCaffeineCache.java} (79%) rename common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/{UserUpdateTimeRedisCache.java => UsersSessionInvalidationRedisCache.java} (75%) delete mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java index d5f62037af..101455f68e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java @@ -19,66 +19,42 @@ import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; -import org.thingsboard.server.cache.TbCacheValueWrapper; +import org.springframework.util.StringUtils; import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.config.JwtSettings; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import java.util.Optional; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; @Service @RequiredArgsConstructor public class TokenOutdatingService { private final TbTransactionalCache cache; private final JwtTokenFactory tokenFactory; - private final JwtSettings jwtSettings; @EventListener(classes = UserAuthDataChangedEvent.class) public void onUserAuthDataChanged(UserAuthDataChangedEvent event) { - cache.put(event.getId(), event.getTs()); + if (StringUtils.hasText(event.getId())) { + cache.put(event.getId(), event.getTs()); + } } public boolean isOutdated(JwtToken token, UserId userId) { Claims claims = tokenFactory.parseTokenClaims(token).getBody(); long issueTime = claims.getIssuedAt().getTime(); - - String sessionId = claims.get("sessionId", String.class); - - Boolean isUserIdOutdated = Optional.ofNullable(cache.get(userId.toString())) - .map(outdatageTimeByUserId -> { - if (refreshTokenNotExpired(outdatageTimeByUserId.get(), System.currentTimeMillis())) { - return accessTokenNotExpired(issueTime, outdatageTimeByUserId.get()); - } else { - return false; - } - }) - .orElse(false); - - if (!isUserIdOutdated) { - return Optional.ofNullable(cache.get(sessionId)).map(outdatageTimeBySessionId -> { - if (refreshTokenNotExpired(outdatageTimeBySessionId.get(), System.currentTimeMillis())) { - return accessTokenNotExpired(issueTime, outdatageTimeBySessionId.get()); - } else { - return false; - } - } - ).orElse(false); - } - - return isUserIdOutdated; + String sessionId = claims.get("sessionId", String.class) == null ? "" : claims.get("sessionId", String.class); + return isTokenOutdated(issueTime, userId.toString()) || isTokenOutdated(issueTime, sessionId); } - private boolean accessTokenNotExpired(long issueTime, Long outdatageTime) { - return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); + private Boolean isTokenOutdated(long issueTime, String sessionId) { + return Optional.ofNullable(cache.get(sessionId)).map(outdatageTime -> isTokenOutdated(issueTime, outdatageTime.get())).orElse(false); } - private boolean refreshTokenNotExpired(Long outdatageTime, long currentTime) { - return currentTime - outdatageTime <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime()); + private boolean isTokenOutdated(long issueTime, Long outdatageTime) { + return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index f5ac40afd5..75703309f0 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -421,7 +421,8 @@ cache: timeToLiveInMinutes: "${CACHE_SPECS_ATTRIBUTES_TTL:1440}" maxSize: "${CACHE_SPECS_ATTRIBUTES_MAX_SIZE:100000}" usersUpdateTime: - timeToLiveInMinutes: "${CACHE_SPECS_USERS_UPDATE_TIME_TTL:20000}" + # MUST be the same as jwt refresh token expiration time, the value here represents 604800 seconds in minutes + timeToLiveInMinutes: "${CACHE_SPECS_USERS_UPDATE_TIME_TTL:10080}" maxSize: "${CACHE_SPECS_USERS_UPDATE_TIME_MAX_SIZE:10000}" otaPackages: timeToLiveInMinutes: "${CACHE_SPECS_OTA_PACKAGES_TTL:60}" diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java index 52371e1755..dddd1f8a71 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java @@ -32,7 +32,6 @@ import org.thingsboard.server.common.data.User; 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.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; import org.thingsboard.server.common.data.security.model.JwtToken; @@ -70,7 +69,8 @@ import static org.mockito.Mockito.when; "security.jwt.tokenIssuer=test.io", "security.jwt.tokenSigningKey=secret", "security.jwt.tokenExpirationTime=600", - "security.jwt.refreshTokenExpTime=10" + "security.jwt.refreshTokenExpTime=60", + "cache.specs.usersUpdateTime.timeToLiveInMinutes=1" }) public class TokenOutdatingTest { private JwtAuthenticationProvider accessTokenAuthenticationProvider; @@ -160,7 +160,7 @@ public class TokenOutdatingTest { assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); - SECONDS.sleep(10); + SECONDS.sleep(60); assertFalse(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationCaffeineCache.java similarity index 79% rename from common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java rename to common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationCaffeineCache.java index a2c07ecaf0..c052627099 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationCaffeineCache.java @@ -24,11 +24,11 @@ import org.thingsboard.server.common.data.CacheConstants; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) -@Service("UsersUpdateTimeCache") -public class UsersUpdateTimeCaffeineCache extends CaffeineTbTransactionalCache { +@Service("UsersSessionInvalidation") +public class UsersSessionInvalidationCaffeineCache extends CaffeineTbTransactionalCache { @Autowired - public UsersUpdateTimeCaffeineCache(CacheManager cacheManager) { - super(cacheManager, CacheConstants.USERS_UPDATE_TIME_CACHE); + public UsersSessionInvalidationCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.USERS_SESSION_INVALIDATION_CACHE); } } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationRedisCache.java similarity index 75% rename from common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java rename to common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationRedisCache.java index c4084773a2..61d1d21516 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationRedisCache.java @@ -26,11 +26,11 @@ import org.thingsboard.server.cache.TbFSTRedisSerializer; import org.thingsboard.server.common.data.CacheConstants; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") -@Service("UsersUpdateTimeCache") -public class UserUpdateTimeRedisCache extends RedisTbTransactionalCache { +@Service("UsersSessionInvalidation") +public class UsersSessionInvalidationRedisCache extends RedisTbTransactionalCache { @Autowired - public UserUpdateTimeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { - super(CacheConstants.USERS_UPDATE_TIME_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>()); + public UsersSessionInvalidationRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.USERS_SESSION_INVALIDATION_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>()); } } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java deleted file mode 100644 index 1b3d980212..0000000000 --- a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.cache.usersUpdateTime; - -import lombok.Data; - -@Data -public class UsersUpdateTimeCacheEvictEvent { - private final String key; -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 0c5ed80cd1..3199b1c0cc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -32,7 +32,7 @@ public class CacheConstants { public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ATTRIBUTES_CACHE = "attributes"; - public static final String USERS_UPDATE_TIME_CACHE = "usersUpdateTime"; + public static final String USERS_SESSION_INVALIDATION_CACHE = "usersUpdateTime"; public static final String OTA_PACKAGE_CACHE = "otaPackages"; public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData"; public static final String REPOSITORY_SETTINGS_CACHE = "repositorySettings"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java index 9ceaa2386e..0436627033 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java @@ -15,9 +15,6 @@ */ package org.thingsboard.server.common.data.security.event; -import lombok.Data; -import org.thingsboard.server.common.data.id.UserId; - import java.io.Serializable; public abstract class UserAuthDataChangedEvent implements Serializable { From 15683504fdd0221c2a2708c96348c22e929bb08b Mon Sep 17 00:00:00 2001 From: Yuriy Lytvynchuk Date: Wed, 2 Nov 2022 15:22:33 +0200 Subject: [PATCH 027/226] refactor & update logic --- .../server/actors/ActorSystemContext.java | 12 + .../actors/ruleChain/DefaultTbContext.java | 23 ++ .../DefaultTbApiUsageStateService.java | 5 + .../apiusage/TbApiUsageStateService.java | 3 +- .../rpc/DefaultTbRuleEngineRpcService.java | 14 +- .../api/RuleEngineApiUsageStateService.java | 26 ++ .../rule/engine/api/RuleEngineRpcService.java | 5 + .../rule/engine/api/TbContext.java | 10 + .../util/EntitiesTenantIdAsyncLoader.java | 98 +++-- .../util/EntitiesTenantIdAsyncLoaderTest.java | 347 ++++++++++++++++++ 10 files changed, 520 insertions(+), 23 deletions(-) create mode 100644 rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineApiUsageStateService.java create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoaderTest.java 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 f4e7eb2463..6fbc2908bd 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -75,6 +75,8 @@ import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.usagestats.TbApiUsageClient; @@ -377,6 +379,16 @@ public class ActorSystemContext { @Getter private QueueService queueService; + @Lazy + @Autowired(required = false) + @Getter + private WidgetsBundleService widgetsBundleService; + + @Lazy + @Autowired(required = false) + @Getter + private WidgetTypeService widgetTypeService; + @Value("${actors.session.max_concurrent_sessions_per_device:1}") @Getter private long maxConcurrentSessionsPerDevice; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 3eec2e942b..7d5ad5744f 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -25,6 +25,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; +import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; import org.thingsboard.rule.engine.api.RuleEngineRpcService; @@ -88,6 +89,8 @@ import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; @@ -719,6 +722,26 @@ class DefaultTbContext implements TbContext { return mainCtx.getTenantProfileCache().get(getTenantId()); } + @Override + public TenantProfile getTenantProfile(TenantId tenantId) { + return mainCtx.getTenantProfileCache().get(tenantId); + } + + @Override + public WidgetsBundleService getWidgetBundleService() { + return mainCtx.getWidgetsBundleService(); + } + + @Override + public WidgetTypeService getWidgetTypeService() { + return mainCtx.getWidgetTypeService(); + } + + @Override + public RuleEngineApiUsageStateService getRuleEngineApiUsageStateService() { + return mainCtx.getApiUsageStateService(); + } + private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("ruleNodeId", ruleNodeId.toString()); diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index f6a590eee2..b8ca8b37c0 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -395,6 +395,11 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService } } + @Override + public ApiUsageState findApiUsageStateById(TenantId tenantId, ApiUsageStateId id) { + return apiUsageStateService.findApiUsageStateById(tenantId, id); + } + private interface StateChecker { boolean check(long threshold, long warnThreshold, long value); } diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java index e6d8ba00ae..1737915f50 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.apiusage; import org.springframework.context.ApplicationListener; +import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -25,7 +26,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceM import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; -public interface TbApiUsageStateService extends ApplicationListener { +public interface TbApiUsageStateService extends RuleEngineApiUsageStateService, ApplicationListener { void process(TbProtoQueueMsg msg, TbCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java index 493ab7ef43..96cc19ef70 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java @@ -15,10 +15,14 @@ */ package org.thingsboard.server.service.rpc; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.id.RpcId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rpc.Rpc; import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest; import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcResponse; @@ -27,6 +31,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest; +import org.thingsboard.server.dao.rpc.RpcService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; @@ -52,6 +57,7 @@ public class DefaultTbRuleEngineRpcService implements TbRuleEngineDeviceRpcServi private final PartitionService partitionService; private final TbClusterService clusterService; private final TbServiceInfoProvider serviceInfoProvider; + private final RpcService rpcService; private final ConcurrentMap> toDeviceRpcRequests = new ConcurrentHashMap<>(); @@ -61,10 +67,11 @@ public class DefaultTbRuleEngineRpcService implements TbRuleEngineDeviceRpcServi public DefaultTbRuleEngineRpcService(PartitionService partitionService, TbClusterService clusterService, - TbServiceInfoProvider serviceInfoProvider) { + TbServiceInfoProvider serviceInfoProvider, RpcService rpcService) { this.partitionService = partitionService; this.clusterService = clusterService; this.serviceInfoProvider = serviceInfoProvider; + this.rpcService = rpcService; } @Autowired(required = false) @@ -119,6 +126,11 @@ public class DefaultTbRuleEngineRpcService implements TbRuleEngineDeviceRpcServi }); } + @Override + public ListenableFuture findRpcByIdAsync(TenantId tenantId, RpcId id) { + return rpcService.findRpcByIdAsync(tenantId, id); + } + @Override public void processRpcResponseFromDevice(FromDeviceRpcResponse response) { log.trace("[{}] Received response to server-side RPC request from Core RPC Service", response.getId()); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineApiUsageStateService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineApiUsageStateService.java new file mode 100644 index 0000000000..03734ffcad --- /dev/null +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineApiUsageStateService.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2022 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.rule.engine.api; + +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.id.ApiUsageStateId; +import org.thingsboard.server.common.data.id.TenantId; + +public interface RuleEngineApiUsageStateService { + + ApiUsageState findApiUsageStateById(TenantId tenantId, ApiUsageStateId id); + +} diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java index abb5ace0f8..aec2cd4e00 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java @@ -15,7 +15,11 @@ */ package org.thingsboard.rule.engine.api; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.RpcId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rpc.Rpc; import java.util.UUID; import java.util.function.Consumer; @@ -29,4 +33,5 @@ public interface RuleEngineRpcService { void sendRpcRequestToDevice(RuleEngineDeviceRpcRequest request, Consumer consumer); + ListenableFuture findRpcByIdAsync(TenantId tenantId, RpcId id); } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 01f61f1e4f..7c539d50f0 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -61,6 +61,8 @@ import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.dao.widget.WidgetsBundleService; import java.util.List; import java.util.Set; @@ -305,4 +307,12 @@ public interface TbContext { void removeListeners(); TenantProfile getTenantProfile(); + + TenantProfile getTenantProfile(TenantId tenantId); + + WidgetsBundleService getWidgetBundleService(); + + WidgetTypeService getWidgetTypeService(); + + RuleEngineApiUsageStateService getRuleEngineApiUsageStateService(); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java index 752ebf5143..3401227987 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java @@ -20,8 +20,10 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CustomerId; @@ -32,50 +34,104 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.OtaPackageId; +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.id.RpcId; import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.rule.RuleNode; + +import java.util.UUID; public class EntitiesTenantIdAsyncLoader { public static ListenableFuture findEntityIdAsync(TbContext ctx, EntityId original) { - ListeningExecutor executor = ctx.getDbCallbackExecutor(); - switch (original.getEntityType()) { + ListenableFuture hasTenantId; + UUID id = original.getId(); + EntityType entityType = original.getEntityType(); + TenantId tenantId = ctx.getTenantId(); + switch (entityType) { case TENANT: - return Futures.immediateFuture((TenantId) original); + return Futures.immediateFuture(new TenantId(id)); case CUSTOMER: - return getTenantAsync(ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), (CustomerId) original), executor); + hasTenantId = ctx.getCustomerService().findCustomerByIdAsync(tenantId, new CustomerId(id)); + break; case USER: - return getTenantAsync(ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) original), executor); + hasTenantId = ctx.getUserService().findUserByIdAsync(tenantId, new UserId(id)); + break; case ASSET: - return getTenantAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) original), executor); + hasTenantId = ctx.getAssetService().findAssetByIdAsync(tenantId, new AssetId(id)); + break; case DEVICE: - return getTenantAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), (DeviceId) original), executor); + hasTenantId = ctx.getDeviceService().findDeviceByIdAsync(tenantId, new DeviceId(id)); + break; case ALARM: - return getTenantAsync(ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), (AlarmId) original), executor); + hasTenantId = ctx.getAlarmService().findAlarmByIdAsync(tenantId, new AlarmId(id)); + break; case RULE_CHAIN: - return getTenantAsync(ctx.getRuleChainService().findRuleChainByIdAsync(ctx.getTenantId(), (RuleChainId) original), executor); + hasTenantId = ctx.getRuleChainService().findRuleChainByIdAsync(tenantId, new RuleChainId(id)); + break; case ENTITY_VIEW: - return getTenantAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), (EntityViewId) original), executor); + hasTenantId = ctx.getEntityViewService().findEntityViewByIdAsync(tenantId, new EntityViewId(id)); + break; case DASHBOARD: - return getTenantAsync(ctx.getDashboardService().findDashboardByIdAsync(ctx.getTenantId(), (DashboardId) original), executor); + hasTenantId = ctx.getDashboardService().findDashboardByIdAsync(tenantId, new DashboardId(id)); + break; case EDGE: - return getTenantAsync(ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), (EdgeId) original), executor); + hasTenantId = ctx.getEdgeService().findEdgeByIdAsync(tenantId, new EdgeId(id)); + break; case OTA_PACKAGE: - return getTenantAsync(ctx.getOtaPackageService().findOtaPackageInfoByIdAsync(ctx.getTenantId(), (OtaPackageId) original), executor); + hasTenantId = ctx.getOtaPackageService().findOtaPackageInfoByIdAsync(tenantId, new OtaPackageId(id)); + break; case ASSET_PROFILE: - return getTenantAsync(Futures.immediateFuture(ctx.getAssetProfileCache().get(ctx.getTenantId(), (AssetProfileId) original)), executor); + hasTenantId = Futures.immediateFuture(ctx.getAssetProfileCache().get(tenantId, new AssetProfileId(id))); + break; case DEVICE_PROFILE: - return getTenantAsync(Futures.immediateFuture(ctx.getDeviceProfileCache().get(ctx.getTenantId(), (DeviceProfileId) original)), executor); + hasTenantId = Futures.immediateFuture(ctx.getDeviceProfileCache().get(tenantId, new DeviceProfileId(id))); + break; + case WIDGET_TYPE: + hasTenantId = Futures.immediateFuture(ctx.getWidgetTypeService().findWidgetTypeById(tenantId, new WidgetTypeId(id))); + break; + case WIDGETS_BUNDLE: + hasTenantId = Futures.immediateFuture(ctx.getWidgetBundleService().findWidgetsBundleById(tenantId, new WidgetsBundleId(id))); + break; + case RPC: + hasTenantId = ctx.getRpcService().findRpcByIdAsync(ctx.getTenantId(), new RpcId(id)); + break; + case QUEUE: + hasTenantId = Futures.immediateFuture(ctx.getQueueService().findQueueById(tenantId, new QueueId(id))); + break; + case API_USAGE_STATE: + hasTenantId = Futures.immediateFuture(ctx.getRuleEngineApiUsageStateService().findApiUsageStateById(tenantId, new ApiUsageStateId(id))); + break; + case TB_RESOURCE: + hasTenantId = ctx.getResourceService().findResourceInfoByIdAsync(tenantId, new TbResourceId(id)); + break; + case RULE_NODE: + hasTenantId = null; + RuleNode ruleNode = ctx.getRuleChainService().findRuleNodeById(tenantId, new RuleNodeId(id)); + if (ruleNode != null) { + return Futures.immediateFuture(tenantId); + } + break; + case TENANT_PROFILE: + hasTenantId = null; + if (ctx.getTenantProfile() == ctx.getTenantProfile(new TenantId(id))) { + return Futures.immediateFuture(tenantId); + } + break; default: - return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original.getEntityType())); + hasTenantId = Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original.getEntityType())); } + return getTenantIdAsync(hasTenantId, ctx.getDbCallbackExecutor()); } - private static ListenableFuture getTenantAsync(ListenableFuture future, ListeningExecutor executor) { - return Futures.transformAsync(future, in -> { - return in != null ? Futures.immediateFuture(in.getTenantId()) - : Futures.immediateFuture(null); - }, executor); + private static ListenableFuture getTenantIdAsync(ListenableFuture future, ListeningExecutor executor) { + return Futures.transformAsync(future, in -> in != null ? Futures.immediateFuture(in.getTenantId()) + : Futures.immediateFuture(null), executor); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoaderTest.java new file mode 100644 index 0000000000..885fa7ec40 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoaderTest.java @@ -0,0 +1,347 @@ +/** + * Copyright © 2016-2022 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.rule.engine.util; + +import com.google.common.util.concurrent.Futures; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.thingsboard.common.util.AbstractListeningExecutor; +import org.thingsboard.rule.engine.api.RuleEngineAlarmService; +import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; +import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; +import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; +import org.thingsboard.rule.engine.api.RuleEngineRpcService; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.OtaPackage; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.rpc.Rpc; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.widget.WidgetType; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.dao.widget.WidgetsBundleService; + +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class EntitiesTenantIdAsyncLoaderTest { + + @Mock + private TbContext ctx; + @Mock + private CustomerService customerService; + @Mock + private UserService userService; + @Mock + private AssetService assetService; + @Mock + private DeviceService deviceService; + @Mock + private RuleEngineAlarmService alarmService; + @Mock + private RuleChainService ruleChainService; + @Mock + private EntityViewService entityViewService; + @Mock + private DashboardService dashboardService; + @Mock + private EdgeService edgeService; + @Mock + private OtaPackageService otaPackageService; + @Mock + private RuleEngineAssetProfileCache assetProfileCache; + @Mock + private RuleEngineDeviceProfileCache deviceProfileCache; + @Mock + private WidgetTypeService widgetTypeService; + @Mock + private WidgetsBundleService widgetsBundleService; + @Mock + private QueueService queueService; + @Mock + private ResourceService resourceService; + @Mock + private RuleEngineRpcService rpcService; + @Mock + private RuleEngineApiUsageStateService ruleEngineApiUsageStateService; + + private TenantId tenantId; + private AbstractListeningExecutor dbExecutor; + + @Before + public void before() { + dbExecutor = new AbstractListeningExecutor() { + @Override + protected int getThreadPollSize() { + return 3; + } + }; + dbExecutor.init(); + this.tenantId = new TenantId(UUID.randomUUID()); + + when(ctx.getTenantId()).thenReturn(tenantId); + when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor); + + for (EntityType entityType : EntityType.values()) { + initMocks(entityType, tenantId); + } + } + + @After + public void after() { + dbExecutor.destroy(); + } + + private void initMocks(EntityType entityType, TenantId tenantId) { + switch (entityType) { + case TENANT: + break; + case CUSTOMER: + Customer customer = new Customer(); + customer.setTenantId(tenantId); + + when(ctx.getCustomerService()).thenReturn(customerService); + doReturn(Futures.immediateFuture(customer)).when(customerService).findCustomerByIdAsync(eq(tenantId), any()); + + break; + case USER: + User user = new User(); + user.setTenantId(tenantId); + + when(ctx.getUserService()).thenReturn(userService); + doReturn(Futures.immediateFuture(user)).when(userService).findUserByIdAsync(eq(tenantId), any()); + + break; + case ASSET: + Asset asset = new Asset(); + asset.setTenantId(tenantId); + + when(ctx.getAssetService()).thenReturn(assetService); + doReturn(Futures.immediateFuture(asset)).when(assetService).findAssetByIdAsync(eq(tenantId), any()); + + break; + case DEVICE: + Device device = new Device(); + device.setTenantId(tenantId); + + when(ctx.getDeviceService()).thenReturn(deviceService); + doReturn(Futures.immediateFuture(device)).when(deviceService).findDeviceByIdAsync(eq(tenantId), any()); + + break; + case ALARM: + Alarm alarm = new Alarm(); + alarm.setTenantId(tenantId); + + when(ctx.getAlarmService()).thenReturn(alarmService); + doReturn(Futures.immediateFuture(alarm)).when(alarmService).findAlarmByIdAsync(eq(tenantId), any()); + + break; + case RULE_CHAIN: + RuleChain ruleChain = new RuleChain(); + ruleChain.setTenantId(tenantId); + + when(ctx.getRuleChainService()).thenReturn(ruleChainService); + doReturn(Futures.immediateFuture(ruleChain)).when(ruleChainService).findRuleChainByIdAsync(eq(tenantId), any()); + + break; + case ENTITY_VIEW: + EntityView entityView = new EntityView(); + entityView.setTenantId(tenantId); + + when(ctx.getEntityViewService()).thenReturn(entityViewService); + doReturn(Futures.immediateFuture(entityView)).when(entityViewService).findEntityViewByIdAsync(eq(tenantId), any()); + + break; + case DASHBOARD: + Dashboard dashboard = new Dashboard(); + dashboard.setTenantId(tenantId); + + when(ctx.getDashboardService()).thenReturn(dashboardService); + doReturn(Futures.immediateFuture(dashboard)).when(dashboardService).findDashboardByIdAsync(eq(tenantId), any()); + + break; + case EDGE: + Edge edge = new Edge(); + edge.setTenantId(tenantId); + + when(ctx.getEdgeService()).thenReturn(edgeService); + doReturn(Futures.immediateFuture(edge)).when(edgeService).findEdgeByIdAsync(eq(tenantId), any()); + + break; + case OTA_PACKAGE: + OtaPackage otaPackage = new OtaPackage(); + otaPackage.setTenantId(tenantId); + + when(ctx.getOtaPackageService()).thenReturn(otaPackageService); + doReturn(Futures.immediateFuture(otaPackage)).when(otaPackageService).findOtaPackageInfoByIdAsync(eq(tenantId), any()); + + break; + case ASSET_PROFILE: + AssetProfile assetProfile = new AssetProfile(); + assetProfile.setTenantId(tenantId); + + when(ctx.getAssetProfileCache()).thenReturn(assetProfileCache); + doReturn(assetProfile).when(assetProfileCache).get(eq(tenantId), any(AssetProfileId.class)); + + break; + case DEVICE_PROFILE: + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setTenantId(tenantId); + + when(ctx.getDeviceProfileCache()).thenReturn(deviceProfileCache); + doReturn(deviceProfile).when(deviceProfileCache).get(eq(tenantId), any(DeviceProfileId.class)); + + break; + case WIDGET_TYPE: + WidgetType widgetType = new WidgetType(); + widgetType.setTenantId(tenantId); + + when(ctx.getWidgetTypeService()).thenReturn(widgetTypeService); + doReturn(widgetType).when(widgetTypeService).findWidgetTypeById(eq(tenantId), any()); + + break; + case WIDGETS_BUNDLE: + WidgetsBundle widgetsBundle = new WidgetsBundle(); + widgetsBundle.setTenantId(tenantId); + + when(ctx.getWidgetBundleService()).thenReturn(widgetsBundleService); + doReturn(widgetsBundle).when(widgetsBundleService).findWidgetsBundleById(eq(tenantId), any()); + + break; + case RPC: + Rpc rps = new Rpc(); + rps.setTenantId(tenantId); + + when(ctx.getRpcService()).thenReturn(rpcService); + doReturn(Futures.immediateFuture(rps)).when(rpcService).findRpcByIdAsync(eq(tenantId), any()); + + break; + case QUEUE: + Queue queue = new Queue(); + queue.setTenantId(tenantId); + + when(ctx.getQueueService()).thenReturn(queueService); + doReturn(queue).when(queueService).findQueueById(eq(tenantId), any()); + + break; + case API_USAGE_STATE: + ApiUsageState apiUsageState = new ApiUsageState(); + apiUsageState.setTenantId(tenantId); + + when(ctx.getRuleEngineApiUsageStateService()).thenReturn(ruleEngineApiUsageStateService); + doReturn(apiUsageState).when(ruleEngineApiUsageStateService).findApiUsageStateById(eq(tenantId), any()); + + break; + case TB_RESOURCE: + TbResource tbResource = new TbResource(); + tbResource.setTenantId(tenantId); + + when(ctx.getResourceService()).thenReturn(resourceService); + doReturn(Futures.immediateFuture(tbResource)).when(resourceService).findResourceInfoByIdAsync(eq(tenantId), any()); + + break; + case RULE_NODE: + RuleNode ruleNode = new RuleNode(); + + when(ctx.getRuleChainService()).thenReturn(ruleChainService); + doReturn(ruleNode).when(ruleChainService).findRuleNodeById(eq(tenantId), any()); + + break; + case TENANT_PROFILE: + TenantProfile tenantProfile = new TenantProfile(); + + when(ctx.getTenantProfile()).thenReturn(tenantProfile); + when(ctx.getTenantProfile(any(TenantId.class))).thenReturn(tenantProfile); + + break; + default: + throw new RuntimeException("Unexpected original EntityType " + entityType); + } + + } + + private EntityId getEntityId(EntityType entityType) { + return EntityIdFactory.getByTypeAndUuid(entityType, UUID.randomUUID()); + } + + private void checkTenant(TenantId checkTenantId, boolean equals) throws ExecutionException, InterruptedException { + for (EntityType entityType : EntityType.values()) { + EntityId entityId = EntityType.TENANT.equals(entityType) ? tenantId : getEntityId(entityType); + TenantId targetTenantId = EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, entityId).get(); + + Assert.assertNotNull(targetTenantId); + String msg = "Check entity type <" + entityType.name() + ">:"; + if (equals) { + Assert.assertEquals(msg, targetTenantId, checkTenantId); + } else { + Assert.assertNotEquals(msg, targetTenantId, checkTenantId); + } + } + } + + @Test + public void test_findEntityIdAsync_current_tenant() throws ExecutionException, InterruptedException { + checkTenant(tenantId, true); + } + + @Test + public void test_findEntityIdAsync_other_tenant() throws ExecutionException, InterruptedException { + checkTenant(new TenantId(UUID.randomUUID()), false); + } + +} From 8b9eb433223aff2c7f75f36619b262e24dfb91af Mon Sep 17 00:00:00 2001 From: Yuriy Lytvynchuk Date: Thu, 3 Nov 2022 14:06:42 +0200 Subject: [PATCH 028/226] refactor code --- .../server/actors/ruleChain/DefaultTbContext.java | 5 ----- .../org/thingsboard/rule/engine/api/TbContext.java | 2 -- .../engine/util/EntitiesTenantIdAsyncLoader.java | 12 +++++++----- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 7d5ad5744f..ac1a9b6758 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -722,11 +722,6 @@ class DefaultTbContext implements TbContext { return mainCtx.getTenantProfileCache().get(getTenantId()); } - @Override - public TenantProfile getTenantProfile(TenantId tenantId) { - return mainCtx.getTenantProfileCache().get(tenantId); - } - @Override public WidgetsBundleService getWidgetBundleService() { return mainCtx.getWidgetsBundleService(); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 7c539d50f0..7578ed1009 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -308,8 +308,6 @@ public interface TbContext { TenantProfile getTenantProfile(); - TenantProfile getTenantProfile(TenantId tenantId); - WidgetsBundleService getWidgetBundleService(); WidgetTypeService getWidgetTypeService(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java index 3401227987..6eb4355dd3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java @@ -100,7 +100,7 @@ public class EntitiesTenantIdAsyncLoader { hasTenantId = Futures.immediateFuture(ctx.getWidgetBundleService().findWidgetsBundleById(tenantId, new WidgetsBundleId(id))); break; case RPC: - hasTenantId = ctx.getRpcService().findRpcByIdAsync(ctx.getTenantId(), new RpcId(id)); + hasTenantId = ctx.getRpcService().findRpcByIdAsync(tenantId, new RpcId(id)); break; case QUEUE: hasTenantId = Futures.immediateFuture(ctx.getQueueService().findQueueById(tenantId, new QueueId(id))); @@ -112,16 +112,18 @@ public class EntitiesTenantIdAsyncLoader { hasTenantId = ctx.getResourceService().findResourceInfoByIdAsync(tenantId, new TbResourceId(id)); break; case RULE_NODE: - hasTenantId = null; RuleNode ruleNode = ctx.getRuleChainService().findRuleNodeById(tenantId, new RuleNodeId(id)); if (ruleNode != null) { - return Futures.immediateFuture(tenantId); + hasTenantId = ctx.getRuleChainService().findRuleChainByIdAsync(tenantId, ruleNode.getRuleChainId()); + } else { + hasTenantId = Futures.immediateFuture(null); } break; case TENANT_PROFILE: - hasTenantId = null; - if (ctx.getTenantProfile() == ctx.getTenantProfile(new TenantId(id))) { + if (ctx.getTenantProfile().getId().equals(id)) { return Futures.immediateFuture(tenantId); + } else { + hasTenantId = Futures.immediateFuture(null); } break; default: From 646504ac50aa43d753b3aff7fa639096f2c79bbc Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Thu, 3 Nov 2022 15:51:18 +0200 Subject: [PATCH 029/226] Tenant Id Loader fixes --- .../actors/ruleChain/DefaultTbContext.java | 19 +-- .../rpc/DefaultTbRuleEngineRpcService.java | 4 +- .../engine/api/RuleEngineAlarmService.java | 2 + .../rule/engine/api/RuleEngineRpcService.java | 2 +- .../rule/engine/api/TbContext.java | 2 + .../rule/engine/debug/TbMsgGeneratorNode.java | 2 +- .../engine/filter/TbCheckRelationNode.java | 2 +- .../util/EntitiesTenantIdAsyncLoader.java | 139 ------------------ .../rule/engine/util/TenantIdLoader.java | 132 +++++++++++++++++ ...oaderTest.java => TenantIdLoaderTest.java} | 58 ++++---- 10 files changed, 178 insertions(+), 184 deletions(-) delete mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java rename rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/{EntitiesTenantIdAsyncLoaderTest.java => TenantIdLoaderTest.java} (82%) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 3e4e459192..dfc248f79b 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -36,7 +36,7 @@ import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; -import org.thingsboard.rule.engine.util.EntitiesTenantIdAsyncLoader; +import org.thingsboard.rule.engine.util.TenantIdLoader; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.cluster.TbClusterService; @@ -776,17 +776,10 @@ class DefaultTbContext implements TbContext { return metaData; } - public void isTenantEntity(EntityId entityId) { - try { - TenantId entityTenantId = EntitiesTenantIdAsyncLoader.findEntityIdAsync(this, entityId).get(); - if (entityTenantId == null) { - throw new RuntimeException("Entity with id '" + entityId + "'not found!"); - } - if (!entityTenantId.equals(this.getTenantId())) { - throw new RuntimeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant."); - } - } catch (Exception e) { - throw new RuntimeException(e); + @Override + public void checkTenantEntity(EntityId entityId) { + if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) { + throw new RuntimeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant."); } } diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java index 96cc19ef70..2acedb9107 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java @@ -127,8 +127,8 @@ public class DefaultTbRuleEngineRpcService implements TbRuleEngineDeviceRpcServi } @Override - public ListenableFuture findRpcByIdAsync(TenantId tenantId, RpcId id) { - return rpcService.findRpcByIdAsync(tenantId, id); + public Rpc findRpcById(TenantId tenantId, RpcId id) { + return rpcService.findById(tenantId, id); } @Override diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java index 69c705c827..21bab255f2 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java @@ -51,6 +51,8 @@ public interface RuleEngineAlarmService { ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId); + Alarm findAlarmById(TenantId tenantId, AlarmId alarmId); + ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java index aec2cd4e00..c2f06c14da 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java @@ -33,5 +33,5 @@ public interface RuleEngineRpcService { void sendRpcRequestToDevice(RuleEngineDeviceRpcRequest request, Consumer consumer); - ListenableFuture findRpcByIdAsync(TenantId tenantId, RpcId id); + Rpc findRpcById(TenantId tenantId, RpcId id); } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 4a47586f64..9851eead80 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -199,6 +199,8 @@ public interface TbContext { * */ + void checkTenantEntity(EntityId entityId); + boolean isLocalEntity(EntityId entityId); RuleNodeId getSelfId(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index f41ba4d8cf..14825af121 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -78,7 +78,7 @@ public class TbMsgGeneratorNode implements TbNode { this.currentMsgCount = 0; if (!StringUtils.isEmpty(config.getOriginatorId())) { originatorId = EntityIdFactory.getByTypeAndUuid(config.getOriginatorType(), config.getOriginatorId()); - ctx.isTenantEntity(originatorId); + ctx.checkTenantEntity(originatorId); } else { originatorId = ctx.getSelfId(); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java index c6707cbd1e..2dacc1a458 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java @@ -62,7 +62,7 @@ public class TbCheckRelationNode implements TbNode { this.config = TbNodeUtils.convert(configuration, TbCheckRelationNodeConfiguration.class); if (config.isCheckForSingleEntity()) { this.singleEntityId = EntityIdFactory.getByTypeAndId(config.getEntityType(), config.getEntityId()); - ctx.isTenantEntity(singleEntityId); + ctx.checkTenantEntity(singleEntityId); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java deleted file mode 100644 index 6eb4355dd3..0000000000 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Copyright © 2016-2022 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.rule.engine.util; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import org.thingsboard.common.util.ListeningExecutor; -import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.id.AlarmId; -import org.thingsboard.server.common.data.id.ApiUsageStateId; -import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.id.AssetProfileId; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.DashboardId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.DeviceProfileId; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.EntityViewId; -import org.thingsboard.server.common.data.id.OtaPackageId; -import org.thingsboard.server.common.data.id.QueueId; -import org.thingsboard.server.common.data.id.RpcId; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.RuleNodeId; -import org.thingsboard.server.common.data.id.TbResourceId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.id.WidgetTypeId; -import org.thingsboard.server.common.data.id.WidgetsBundleId; -import org.thingsboard.server.common.data.rule.RuleNode; - -import java.util.UUID; - -public class EntitiesTenantIdAsyncLoader { - - public static ListenableFuture findEntityIdAsync(TbContext ctx, EntityId original) { - ListenableFuture hasTenantId; - UUID id = original.getId(); - EntityType entityType = original.getEntityType(); - TenantId tenantId = ctx.getTenantId(); - switch (entityType) { - case TENANT: - return Futures.immediateFuture(new TenantId(id)); - case CUSTOMER: - hasTenantId = ctx.getCustomerService().findCustomerByIdAsync(tenantId, new CustomerId(id)); - break; - case USER: - hasTenantId = ctx.getUserService().findUserByIdAsync(tenantId, new UserId(id)); - break; - case ASSET: - hasTenantId = ctx.getAssetService().findAssetByIdAsync(tenantId, new AssetId(id)); - break; - case DEVICE: - hasTenantId = ctx.getDeviceService().findDeviceByIdAsync(tenantId, new DeviceId(id)); - break; - case ALARM: - hasTenantId = ctx.getAlarmService().findAlarmByIdAsync(tenantId, new AlarmId(id)); - break; - case RULE_CHAIN: - hasTenantId = ctx.getRuleChainService().findRuleChainByIdAsync(tenantId, new RuleChainId(id)); - break; - case ENTITY_VIEW: - hasTenantId = ctx.getEntityViewService().findEntityViewByIdAsync(tenantId, new EntityViewId(id)); - break; - case DASHBOARD: - hasTenantId = ctx.getDashboardService().findDashboardByIdAsync(tenantId, new DashboardId(id)); - break; - case EDGE: - hasTenantId = ctx.getEdgeService().findEdgeByIdAsync(tenantId, new EdgeId(id)); - break; - case OTA_PACKAGE: - hasTenantId = ctx.getOtaPackageService().findOtaPackageInfoByIdAsync(tenantId, new OtaPackageId(id)); - break; - case ASSET_PROFILE: - hasTenantId = Futures.immediateFuture(ctx.getAssetProfileCache().get(tenantId, new AssetProfileId(id))); - break; - case DEVICE_PROFILE: - hasTenantId = Futures.immediateFuture(ctx.getDeviceProfileCache().get(tenantId, new DeviceProfileId(id))); - break; - case WIDGET_TYPE: - hasTenantId = Futures.immediateFuture(ctx.getWidgetTypeService().findWidgetTypeById(tenantId, new WidgetTypeId(id))); - break; - case WIDGETS_BUNDLE: - hasTenantId = Futures.immediateFuture(ctx.getWidgetBundleService().findWidgetsBundleById(tenantId, new WidgetsBundleId(id))); - break; - case RPC: - hasTenantId = ctx.getRpcService().findRpcByIdAsync(tenantId, new RpcId(id)); - break; - case QUEUE: - hasTenantId = Futures.immediateFuture(ctx.getQueueService().findQueueById(tenantId, new QueueId(id))); - break; - case API_USAGE_STATE: - hasTenantId = Futures.immediateFuture(ctx.getRuleEngineApiUsageStateService().findApiUsageStateById(tenantId, new ApiUsageStateId(id))); - break; - case TB_RESOURCE: - hasTenantId = ctx.getResourceService().findResourceInfoByIdAsync(tenantId, new TbResourceId(id)); - break; - case RULE_NODE: - RuleNode ruleNode = ctx.getRuleChainService().findRuleNodeById(tenantId, new RuleNodeId(id)); - if (ruleNode != null) { - hasTenantId = ctx.getRuleChainService().findRuleChainByIdAsync(tenantId, ruleNode.getRuleChainId()); - } else { - hasTenantId = Futures.immediateFuture(null); - } - break; - case TENANT_PROFILE: - if (ctx.getTenantProfile().getId().equals(id)) { - return Futures.immediateFuture(tenantId); - } else { - hasTenantId = Futures.immediateFuture(null); - } - break; - default: - hasTenantId = Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original.getEntityType())); - } - return getTenantIdAsync(hasTenantId, ctx.getDbCallbackExecutor()); - } - - private static ListenableFuture getTenantIdAsync(ListenableFuture future, ListeningExecutor executor) { - return Futures.transformAsync(future, in -> in != null ? Futures.immediateFuture(in.getTenantId()) - : Futures.immediateFuture(null), executor); - } -} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java new file mode 100644 index 0000000000..b687246789 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -0,0 +1,132 @@ +/** + * Copyright © 2016-2022 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.rule.engine.util; + +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.ApiUsageStateId; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.OtaPackageId; +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.id.RpcId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.rule.RuleNode; + +import java.util.UUID; + +public class TenantIdLoader { + + public static TenantId findTenantId(TbContext ctx, EntityId entityId) { + UUID id = entityId.getId(); + EntityType entityType = entityId.getEntityType(); + TenantId ctxTenantId = ctx.getTenantId(); + + HasTenantId tenantEntity; + switch (entityType) { + case TENANT: + return new TenantId(id); + case CUSTOMER: + tenantEntity = ctx.getCustomerService().findCustomerById(ctxTenantId, new CustomerId(id)); + break; + case USER: + tenantEntity = ctx.getUserService().findUserById(ctxTenantId, new UserId(id)); + break; + case ASSET: + tenantEntity = ctx.getAssetService().findAssetById(ctxTenantId, new AssetId(id)); + break; + case DEVICE: + tenantEntity = ctx.getDeviceService().findDeviceById(ctxTenantId, new DeviceId(id)); + break; + case ALARM: + tenantEntity = ctx.getAlarmService().findAlarmById(ctxTenantId, new AlarmId(id)); + break; + case RULE_CHAIN: + tenantEntity = ctx.getRuleChainService().findRuleChainById(ctxTenantId, new RuleChainId(id)); + break; + case ENTITY_VIEW: + tenantEntity = ctx.getEntityViewService().findEntityViewById(ctxTenantId, new EntityViewId(id)); + break; + case DASHBOARD: + tenantEntity = ctx.getDashboardService().findDashboardById(ctxTenantId, new DashboardId(id)); + break; + case EDGE: + tenantEntity = ctx.getEdgeService().findEdgeById(ctxTenantId, new EdgeId(id)); + break; + case OTA_PACKAGE: + tenantEntity = ctx.getOtaPackageService().findOtaPackageInfoById(ctxTenantId, new OtaPackageId(id)); + break; + case ASSET_PROFILE: + tenantEntity = ctx.getAssetProfileCache().get(ctxTenantId, new AssetProfileId(id)); + break; + case DEVICE_PROFILE: + tenantEntity = ctx.getDeviceProfileCache().get(ctxTenantId, new DeviceProfileId(id)); + break; + case WIDGET_TYPE: + tenantEntity = ctx.getWidgetTypeService().findWidgetTypeById(ctxTenantId, new WidgetTypeId(id)); + break; + case WIDGETS_BUNDLE: + tenantEntity = ctx.getWidgetBundleService().findWidgetsBundleById(ctxTenantId, new WidgetsBundleId(id)); + break; + case RPC: + tenantEntity = ctx.getRpcService().findRpcById(ctxTenantId, new RpcId(id)); + break; + case QUEUE: + tenantEntity = ctx.getQueueService().findQueueById(ctxTenantId, new QueueId(id)); + break; + case API_USAGE_STATE: + tenantEntity = ctx.getRuleEngineApiUsageStateService().findApiUsageStateById(ctxTenantId, new ApiUsageStateId(id)); + break; + case TB_RESOURCE: + tenantEntity = ctx.getResourceService().findResourceInfoById(ctxTenantId, new TbResourceId(id)); + break; + case RULE_NODE: + RuleNode ruleNode = ctx.getRuleChainService().findRuleNodeById(ctxTenantId, new RuleNodeId(id)); + if (ruleNode != null) { + tenantEntity = ctx.getRuleChainService().findRuleChainById(ctxTenantId, ruleNode.getRuleChainId()); + } else { + tenantEntity = null; + } + break; + case TENANT_PROFILE: + if (ctx.getTenantProfile().getId().equals(entityId)) { + return ctxTenantId; + } else { + tenantEntity = null; + } + break; + default: + throw new RuntimeException("Unexpected entity type: " + entityId.getEntityType()); + } + return tenantEntity != null ? tenantEntity.getTenantId() : null; + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java similarity index 82% rename from rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoaderTest.java rename to rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 885fa7ec40..00e8a8c6c8 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.util; -import com.google.common.util.concurrent.Futures; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -50,6 +49,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rpc.Rpc; import org.thingsboard.server.common.data.rule.RuleChain; @@ -71,7 +71,6 @@ import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import java.util.UUID; -import java.util.concurrent.ExecutionException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -79,7 +78,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) -public class EntitiesTenantIdAsyncLoaderTest { +public class TenantIdLoaderTest { @Mock private TbContext ctx; @@ -121,6 +120,7 @@ public class EntitiesTenantIdAsyncLoaderTest { private RuleEngineApiUsageStateService ruleEngineApiUsageStateService; private TenantId tenantId; + private TenantProfileId tenantProfileId; private AbstractListeningExecutor dbExecutor; @Before @@ -133,9 +133,9 @@ public class EntitiesTenantIdAsyncLoaderTest { }; dbExecutor.init(); this.tenantId = new TenantId(UUID.randomUUID()); + this.tenantProfileId = new TenantProfileId(UUID.randomUUID()); when(ctx.getTenantId()).thenReturn(tenantId); - when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor); for (EntityType entityType : EntityType.values()) { initMocks(entityType, tenantId); @@ -156,7 +156,7 @@ public class EntitiesTenantIdAsyncLoaderTest { customer.setTenantId(tenantId); when(ctx.getCustomerService()).thenReturn(customerService); - doReturn(Futures.immediateFuture(customer)).when(customerService).findCustomerByIdAsync(eq(tenantId), any()); + doReturn(customer).when(customerService).findCustomerById(eq(tenantId), any()); break; case USER: @@ -164,7 +164,7 @@ public class EntitiesTenantIdAsyncLoaderTest { user.setTenantId(tenantId); when(ctx.getUserService()).thenReturn(userService); - doReturn(Futures.immediateFuture(user)).when(userService).findUserByIdAsync(eq(tenantId), any()); + doReturn(user).when(userService).findUserById(eq(tenantId), any()); break; case ASSET: @@ -172,7 +172,7 @@ public class EntitiesTenantIdAsyncLoaderTest { asset.setTenantId(tenantId); when(ctx.getAssetService()).thenReturn(assetService); - doReturn(Futures.immediateFuture(asset)).when(assetService).findAssetByIdAsync(eq(tenantId), any()); + doReturn(asset).when(assetService).findAssetById(eq(tenantId), any()); break; case DEVICE: @@ -180,7 +180,7 @@ public class EntitiesTenantIdAsyncLoaderTest { device.setTenantId(tenantId); when(ctx.getDeviceService()).thenReturn(deviceService); - doReturn(Futures.immediateFuture(device)).when(deviceService).findDeviceByIdAsync(eq(tenantId), any()); + doReturn(device).when(deviceService).findDeviceById(eq(tenantId), any()); break; case ALARM: @@ -188,7 +188,7 @@ public class EntitiesTenantIdAsyncLoaderTest { alarm.setTenantId(tenantId); when(ctx.getAlarmService()).thenReturn(alarmService); - doReturn(Futures.immediateFuture(alarm)).when(alarmService).findAlarmByIdAsync(eq(tenantId), any()); + doReturn(alarm).when(alarmService).findAlarmById(eq(tenantId), any()); break; case RULE_CHAIN: @@ -196,7 +196,7 @@ public class EntitiesTenantIdAsyncLoaderTest { ruleChain.setTenantId(tenantId); when(ctx.getRuleChainService()).thenReturn(ruleChainService); - doReturn(Futures.immediateFuture(ruleChain)).when(ruleChainService).findRuleChainByIdAsync(eq(tenantId), any()); + doReturn(ruleChain).when(ruleChainService).findRuleChainById(eq(tenantId), any()); break; case ENTITY_VIEW: @@ -204,7 +204,7 @@ public class EntitiesTenantIdAsyncLoaderTest { entityView.setTenantId(tenantId); when(ctx.getEntityViewService()).thenReturn(entityViewService); - doReturn(Futures.immediateFuture(entityView)).when(entityViewService).findEntityViewByIdAsync(eq(tenantId), any()); + doReturn(entityView).when(entityViewService).findEntityViewById(eq(tenantId), any()); break; case DASHBOARD: @@ -212,7 +212,7 @@ public class EntitiesTenantIdAsyncLoaderTest { dashboard.setTenantId(tenantId); when(ctx.getDashboardService()).thenReturn(dashboardService); - doReturn(Futures.immediateFuture(dashboard)).when(dashboardService).findDashboardByIdAsync(eq(tenantId), any()); + doReturn(dashboard).when(dashboardService).findDashboardById(eq(tenantId), any()); break; case EDGE: @@ -220,7 +220,7 @@ public class EntitiesTenantIdAsyncLoaderTest { edge.setTenantId(tenantId); when(ctx.getEdgeService()).thenReturn(edgeService); - doReturn(Futures.immediateFuture(edge)).when(edgeService).findEdgeByIdAsync(eq(tenantId), any()); + doReturn(edge).when(edgeService).findEdgeById(eq(tenantId), any()); break; case OTA_PACKAGE: @@ -228,7 +228,7 @@ public class EntitiesTenantIdAsyncLoaderTest { otaPackage.setTenantId(tenantId); when(ctx.getOtaPackageService()).thenReturn(otaPackageService); - doReturn(Futures.immediateFuture(otaPackage)).when(otaPackageService).findOtaPackageInfoByIdAsync(eq(tenantId), any()); + doReturn(otaPackage).when(otaPackageService).findOtaPackageInfoById(eq(tenantId), any()); break; case ASSET_PROFILE: @@ -264,11 +264,11 @@ public class EntitiesTenantIdAsyncLoaderTest { break; case RPC: - Rpc rps = new Rpc(); - rps.setTenantId(tenantId); + Rpc rpc = new Rpc(); + rpc.setTenantId(tenantId); when(ctx.getRpcService()).thenReturn(rpcService); - doReturn(Futures.immediateFuture(rps)).when(rpcService).findRpcByIdAsync(eq(tenantId), any()); + doReturn(rpc).when(rpcService).findRpcById(eq(tenantId), any()); break; case QUEUE: @@ -292,7 +292,7 @@ public class EntitiesTenantIdAsyncLoaderTest { tbResource.setTenantId(tenantId); when(ctx.getResourceService()).thenReturn(resourceService); - doReturn(Futures.immediateFuture(tbResource)).when(resourceService).findResourceInfoByIdAsync(eq(tenantId), any()); + doReturn(tbResource).when(resourceService).findResourceInfoById(eq(tenantId), any()); break; case RULE_NODE: @@ -303,10 +303,9 @@ public class EntitiesTenantIdAsyncLoaderTest { break; case TENANT_PROFILE: - TenantProfile tenantProfile = new TenantProfile(); + TenantProfile tenantProfile = new TenantProfile(tenantProfileId); when(ctx.getTenantProfile()).thenReturn(tenantProfile); - when(ctx.getTenantProfile(any(TenantId.class))).thenReturn(tenantProfile); break; default: @@ -319,12 +318,17 @@ public class EntitiesTenantIdAsyncLoaderTest { return EntityIdFactory.getByTypeAndUuid(entityType, UUID.randomUUID()); } - private void checkTenant(TenantId checkTenantId, boolean equals) throws ExecutionException, InterruptedException { + private void checkTenant(TenantId checkTenantId, boolean equals) { for (EntityType entityType : EntityType.values()) { - EntityId entityId = EntityType.TENANT.equals(entityType) ? tenantId : getEntityId(entityType); - TenantId targetTenantId = EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, entityId).get(); - - Assert.assertNotNull(targetTenantId); + EntityId entityId; + if(EntityType.TENANT.equals(entityType)){ + entityId = tenantId; + } else if(EntityType.TENANT_PROFILE.equals(entityType)){ + entityId = tenantProfileId; + } else { + entityId = getEntityId(entityType); + } + TenantId targetTenantId = TenantIdLoader.findTenantId(ctx, entityId); String msg = "Check entity type <" + entityType.name() + ">:"; if (equals) { Assert.assertEquals(msg, targetTenantId, checkTenantId); @@ -335,12 +339,12 @@ public class EntitiesTenantIdAsyncLoaderTest { } @Test - public void test_findEntityIdAsync_current_tenant() throws ExecutionException, InterruptedException { + public void test_findEntityIdAsync_current_tenant() { checkTenant(tenantId, true); } @Test - public void test_findEntityIdAsync_other_tenant() throws ExecutionException, InterruptedException { + public void test_findEntityIdAsync_other_tenant() { checkTenant(new TenantId(UUID.randomUUID()), false); } From 4954de20224aa859f303fb78b94d54192f744b25 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 3 Nov 2022 16:48:37 +0200 Subject: [PATCH 030/226] Avoid using 'nulls first' or 'nulls last' for ts_kv --- application/src/main/resources/thingsboard.yml | 2 +- .../AbstractChunkedAggregationTimeseriesDao.java | 5 ++--- .../sqlts/timescale/TimescaleTimeseriesDao.java | 4 +--- .../sqlts/timescale/TsKvTimescaleRepository.java | 15 +++++++-------- .../server/dao/sqlts/ts/TsKvRepository.java | 11 +++++++++-- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 66a603a1a1..75e6b777c8 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -531,7 +531,7 @@ spring.servlet.multipart.max-file-size: "50MB" spring.servlet.multipart.max-request-size: "50MB" spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation: "true" -spring.jpa.properties.hibernate.order_by.default_null_ordering: "${SPRING_JPA_PROPERTIES_HIBERNATE_ORDER_BY_DEFAULT_NULL_ORDERING:last}" +spring.jpa.properties.hibernate.order_by.default_null_ordering: "${SPRING_JPA_PROPERTIES_HIBERNATE_ORDER_BY_DEFAULT_NULL_ORDERING:last}" # Note: as for current Spring JPA version, custom NullHandling for the Sort.Order is ignored and this default is used # SQL DAO Configuration spring: diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java index 22c4661fcd..cf726c32c4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java @@ -20,7 +20,7 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.Aggregation; @@ -143,8 +143,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq keyId, query.getStartTs(), query.getEndTs(), - PageRequest.of(0, query.getLimit(), - Sort.by(new Sort.Order(Sort.Direction.fromString(query.getOrder()), "ts").nullsNative()))); + PageRequest.ofSize(query.getLimit()).withSort(Direction.fromString(query.getOrder()), "ts")); tsKvEntities.forEach(tsKvEntity -> tsKvEntity.setStrKey(query.getKey())); List tsKvEntries = DaoUtil.convertDataList(tsKvEntities); long lastTs = tsKvEntries.stream().map(TsKvEntry::getTs).max(Long::compare).orElse(query.getStartTs()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java index 6e9f9e61b8..c9daf1ec6d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java @@ -175,9 +175,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements keyId, query.getStartTs(), query.getEndTs(), - PageRequest.of(0, query.getLimit(), - Sort.by(new Sort.Order(Sort.Direction.fromString(query.getOrder()), "ts").nullsNative()))); - ; + PageRequest.ofSize(query.getLimit()).withSort(Sort.Direction.fromString(query.getOrder()), "ts")); timescaleTsKvEntities.forEach(tsKvEntity -> tsKvEntity.setStrKey(strKey)); var tsKvEntries = DaoUtil.convertDataList(timescaleTsKvEntities); long lastTs = tsKvEntries.stream().map(TsKvEntry::getTs).max(Long::compare).orElse(query.getStartTs()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java index 075cffcbc7..821833cad0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java @@ -31,14 +31,13 @@ import java.util.UUID; @TimescaleDBTsOrTsLatestDao public interface TsKvTimescaleRepository extends JpaRepository { - @Query("SELECT tskv FROM TimescaleTsKvEntity tskv WHERE tskv.entityId = :entityId " + - "AND tskv.key = :entityKey " + - "AND tskv.ts >= :startTs AND tskv.ts < :endTs") - List findAllWithLimit( - @Param("entityId") UUID entityId, - @Param("entityKey") int key, - @Param("startTs") long startTs, - @Param("endTs") long endTs, Pageable pageable); + @Query(value = "SELECT * FROM ts_kv WHERE entity_id = :entityId " + + "AND key = :entityKey AND ts >= :startTs AND ts < :endTs", nativeQuery = true) + List findAllWithLimit(@Param("entityId") UUID entityId, + @Param("entityKey") int key, + @Param("startTs") long startTs, + @Param("endTs") long endTs, + Pageable pageable); @Transactional @Modifying diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java index 1d9817a5bd..66d619d0ea 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java @@ -29,8 +29,15 @@ import java.util.UUID; public interface TsKvRepository extends JpaRepository { - @Query("SELECT tskv FROM TsKvEntity tskv WHERE tskv.entityId = :entityId " + - "AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") + /* + * Using native query to avoid adding 'nulls first' or 'nulls last' (ignoring spring.jpa.properties.hibernate.order_by.default_null_ordering) + * to the order so that index scan is done instead of full scan. + * + * Note: even when setting custom NullHandling for the Sort.Order for non-native queries, + * it will be ignored and default_null_ordering will be used + * */ + @Query(value = "SELECT * FROM ts_kv WHERE entity_id = :entityId " + + "AND key = :entityKey AND ts >= :startTs AND ts < :endTs ", nativeQuery = true) List findAllWithLimit(@Param("entityId") UUID entityId, @Param("entityKey") int key, @Param("startTs") long startTs, From d9023d701766d76fe02649fd59a4d6d8d503441e Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 3 Nov 2022 17:21:54 +0200 Subject: [PATCH 031/226] Fix comment for hibernate.order_by.default_null_ordering property --- application/src/main/resources/thingsboard.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 75e6b777c8..b9130127ae 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -531,7 +531,8 @@ spring.servlet.multipart.max-file-size: "50MB" spring.servlet.multipart.max-request-size: "50MB" spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation: "true" -spring.jpa.properties.hibernate.order_by.default_null_ordering: "${SPRING_JPA_PROPERTIES_HIBERNATE_ORDER_BY_DEFAULT_NULL_ORDERING:last}" # Note: as for current Spring JPA version, custom NullHandling for the Sort.Order is ignored and this default is used +# Note: as for current Spring JPA version, custom NullHandling for the Sort.Order is ignored and this parameter is used +spring.jpa.properties.hibernate.order_by.default_null_ordering: "${SPRING_JPA_PROPERTIES_HIBERNATE_ORDER_BY_DEFAULT_NULL_ORDERING:last}" # SQL DAO Configuration spring: From 4fad5879404d3b2f56bc831c4d65fc79faf3010d Mon Sep 17 00:00:00 2001 From: oyurov Date: Thu, 3 Nov 2022 16:52:15 +0100 Subject: [PATCH 032/226] Refactoring --- .../auth/DefaultTokenOutdatingService.java | 65 +++++++++++++++++++ .../security/auth/TokenOutdatingService.java | 40 +----------- .../auth/jwt/JwtAuthenticationProvider.java | 2 +- .../RefreshTokenAuthenticationProvider.java | 2 +- .../UserCredentialsInvalidationEvent.java | 2 - 5 files changed, 70 insertions(+), 41 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java new file mode 100644 index 0000000000..fecc17d33b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2022 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.security.auth; + +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.TbTransactionalCache; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; +import org.thingsboard.server.common.data.security.model.JwtToken; +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; + +import java.util.Optional; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +@Service +@RequiredArgsConstructor +public class DefaultTokenOutdatingService implements TokenOutdatingService { + private final TbTransactionalCache cache; + private final JwtTokenFactory tokenFactory; + + @EventListener(classes = UserAuthDataChangedEvent.class) + public void onUserAuthDataChanged(UserAuthDataChangedEvent event) { + if (StringUtils.hasText(event.getId())) { + cache.put(event.getId(), event.getTs()); + } + } + + @Override + public boolean isOutdated(JwtToken token, UserId userId) { + Claims claims = tokenFactory.parseTokenClaims(token).getBody(); + long issueTime = claims.getIssuedAt().getTime(); + String sessionId = claims.get("sessionId", String.class); + if (sessionId == null) { + return isTokenOutdated(issueTime, userId.toString()); + } else { + return isTokenOutdated(issueTime, userId.toString()) || isTokenOutdated(issueTime, sessionId); + } + } + + private Boolean isTokenOutdated(long issueTime, String sessionId) { + return Optional.ofNullable(cache.get(sessionId)).map(outdatageTime -> isTokenOutdated(issueTime, outdatageTime.get())).orElse(false); + } + + private boolean isTokenOutdated(long issueTime, Long outdatageTime) { + return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java index 101455f68e..9013a85553 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java @@ -15,46 +15,12 @@ */ package org.thingsboard.server.service.security.auth; -import io.jsonwebtoken.Claims; -import lombok.RequiredArgsConstructor; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; -import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.service.security.model.token.JwtTokenFactory; -import java.util.Optional; +public interface TokenOutdatingService { + void onUserAuthDataChanged(UserAuthDataChangedEvent event); -import static java.util.concurrent.TimeUnit.MILLISECONDS; - -@Service -@RequiredArgsConstructor -public class TokenOutdatingService { - private final TbTransactionalCache cache; - private final JwtTokenFactory tokenFactory; - - @EventListener(classes = UserAuthDataChangedEvent.class) - public void onUserAuthDataChanged(UserAuthDataChangedEvent event) { - if (StringUtils.hasText(event.getId())) { - cache.put(event.getId(), event.getTs()); - } - } - - public boolean isOutdated(JwtToken token, UserId userId) { - Claims claims = tokenFactory.parseTokenClaims(token).getBody(); - long issueTime = claims.getIssuedAt().getTime(); - String sessionId = claims.get("sessionId", String.class) == null ? "" : claims.get("sessionId", String.class); - return isTokenOutdated(issueTime, userId.toString()) || isTokenOutdated(issueTime, sessionId); - } - - private Boolean isTokenOutdated(long issueTime, String sessionId) { - return Optional.ofNullable(cache.get(sessionId)).map(outdatageTime -> isTokenOutdated(issueTime, outdatageTime.get())).orElse(false); - } - - private boolean isTokenOutdated(long issueTime, Long outdatageTime) { - return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); - } + boolean isOutdated(JwtToken token, UserId userId); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java index dcdce946e0..9ce2ae75fc 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java @@ -20,8 +20,8 @@ import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.stereotype.Component; -import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.service.security.auth.JwtAuthenticationToken; +import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index 700bbd7f74..27549752e4 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -33,11 +33,11 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; +import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java index 26eac46fc9..63ee49e53b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java @@ -16,10 +16,8 @@ package org.thingsboard.server.common.data.security.event; import lombok.EqualsAndHashCode; -import lombok.Getter; import org.thingsboard.server.common.data.id.UserId; -@Getter @EqualsAndHashCode(callSuper = true) public class UserCredentialsInvalidationEvent extends UserAuthDataChangedEvent { private final UserId userId; From 52d7da7afd934d6a21002b4794c5730c21399cf2 Mon Sep 17 00:00:00 2001 From: dlandiak Date: Fri, 4 Nov 2022 10:33:08 +0200 Subject: [PATCH 033/226] inactivity timeout fix - get from attr when persist to telemetry and default found --- .../state/DefaultDeviceStateService.java | 27 +++++++++++- .../state/DefaultDeviceStateServiceTest.java | 41 ++++++++++++++++++- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index c9c54161dd..38f4ee50c7 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -24,6 +24,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -83,6 +84,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.UUID; @@ -152,6 +154,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService attributeOpt = attributesService.find(TenantId.SYS_TENANT_ID, deviceStateData.getDeviceId(), SERVER_SCOPE, INACTIVITY_TIMEOUT).get(); + attributeOpt.flatMap(KvEntry::getLongValue).ifPresent((inactivityTimeout) -> { + if (inactivityTimeout > 0) { + deviceStateData.getState().setInactivityTimeout(inactivityTimeout); + } + }); + } catch (Exception e) { + log.warn("[{}] Failed to fetch inactivity timeout from attribute", deviceStateData.getDeviceId(), e); + } + } } private EntityKeyType getKeyType() { diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index d5d74f2512..bc0e067442 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -15,27 +15,45 @@ */ package org.thingsboard.server.service.state; +import com.google.common.util.concurrent.Futures; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.willReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; +import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; +import static org.thingsboard.server.service.state.DefaultDeviceStateService.INACTIVITY_TIMEOUT; @RunWith(MockitoJUnitRunner.class) public class DefaultDeviceStateServiceTest { @@ -83,4 +101,25 @@ public class DefaultDeviceStateServiceTest { Mockito.verify(service, times(1)).fetchDeviceStateDataUsingEntityDataQuery(deviceId); } + @Test + public void givenPersistToTelemetryAndDefaultInactivityTimeoutFetched_whenTransformingToDeviceStateData_thenTryGetInactivityFromAttribute() { + var defaultInactivityTimeoutInSec = 60L; + service.setDefaultInactivityTimeoutInSec(defaultInactivityTimeoutInSec); + service.setPersistToTelemetry(true); + + var deviceUuid = UUID.randomUUID(); + var deviceId = new DeviceId(deviceUuid); + + when(attributesService.find(any(), any(), anyString(), anyString())) + .thenReturn(Futures.immediateFuture(Optional.of(new BaseAttributeKvEntry(0, new LongDataEntry(INACTIVITY_TIMEOUT, 5000L))))); + + var latest = + Map.of(EntityKeyType.TIME_SERIES, Map.of(INACTIVITY_TIMEOUT, new TsValue(0, Long.toString(defaultInactivityTimeoutInSec * 1000)))); + DeviceStateData deviceStateData = service.toDeviceStateData(new EntityData(deviceId, latest, Map.of()), new DeviceIdInfo(TenantId.SYS_TENANT_ID.getId(), UUID.randomUUID(), deviceUuid)); + + Mockito.verify(attributesService, times(1)).find(TenantId.SYS_TENANT_ID, deviceId, SERVER_SCOPE, INACTIVITY_TIMEOUT); + + Assert.assertEquals(5000L, deviceStateData.getState().getInactivityTimeout()); + } + } \ No newline at end of file From 2f83810fe4aaae8cae7e1048b0e423abc45bd0f5 Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Fri, 4 Nov 2022 14:54:56 +0200 Subject: [PATCH 034/226] fix compilation error in alarm service & code cleanup --- .../server/actors/ruleChain/DefaultTbContext.java | 2 +- .../service/apiusage/TbApiUsageStateService.java | 3 --- .../service/rpc/DefaultTbRuleEngineRpcService.java | 4 ++-- .../telemetry/DefaultAlarmSubscriptionService.java | 10 ++++++++++ .../rule/engine/api/RuleEngineRpcService.java | 2 -- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index dfc248f79b..677a9c2641 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java index 668dc288a0..5f01d34877 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.apiusage; import org.springframework.context.ApplicationListener; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; -import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; @@ -31,8 +30,6 @@ public interface TbApiUsageStateService extends TbApiUsageStateClient, RuleEngin void process(TbProtoQueueMsg msg, TbCallback callback); - ApiUsageState getApiUsageState(TenantId tenantId); - void onTenantProfileUpdate(TenantProfileId tenantProfileId); void onTenantUpdate(TenantId tenantId); diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java index 2acedb9107..c0f6cc750f 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.rpc; -import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -67,7 +66,8 @@ public class DefaultTbRuleEngineRpcService implements TbRuleEngineDeviceRpcServi public DefaultTbRuleEngineRpcService(PartitionService partitionService, TbClusterService clusterService, - TbServiceInfoProvider serviceInfoProvider, RpcService rpcService) { + TbServiceInfoProvider serviceInfoProvider, + RpcService rpcService) { this.partitionService = partitionService; this.clusterService = clusterService; this.serviceInfoProvider = serviceInfoProvider; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java index 9c66d14011..d0279c1a87 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -52,6 +52,7 @@ import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import java.util.Collection; import java.util.Optional; +import java.util.concurrent.ExecutionException; /** * Created by ashvayka on 27.03.18. @@ -129,6 +130,15 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService return alarmService.findAlarmByIdAsync(tenantId, alarmId); } + @Override + public Alarm findAlarmById(TenantId tenantId, AlarmId alarmId) { + try { + return alarmService.findAlarmByIdAsync(tenantId, alarmId).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + @Override public ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId) { return alarmService.findAlarmInfoByIdAsync(tenantId, alarmId); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java index c2f06c14da..779e958793 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java @@ -15,8 +15,6 @@ */ package org.thingsboard.rule.engine.api; -import com.google.common.util.concurrent.ListenableFuture; -import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.RpcId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rpc.Rpc; From ee98f203ef455d8329b71f7c6b1504b3a3248f14 Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Fri, 4 Nov 2022 15:14:43 +0200 Subject: [PATCH 035/226] added findAlarmById method to AlarmService --- .../telemetry/DefaultAlarmSubscriptionService.java | 6 +----- .../org/thingsboard/server/dao/alarm/AlarmService.java | 2 ++ .../thingsboard/server/dao/alarm/BaseAlarmService.java | 9 ++++++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java index d0279c1a87..64d4630314 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -132,11 +132,7 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService @Override public Alarm findAlarmById(TenantId tenantId, AlarmId alarmId) { - try { - return alarmService.findAlarmByIdAsync(tenantId, alarmId).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } + return alarmService.findAlarmById(tenantId, alarmId); } @Override diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index 02cd1a8348..b8041aa3fc 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -48,6 +48,8 @@ public interface AlarmService { ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); + Alarm findAlarmById(TenantId tenantId, AlarmId alarmId); + ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId); ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 1316e52405..e6012602f1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -262,9 +262,16 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } @Override - public ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId) { + public Alarm findAlarmById(TenantId tenantId, AlarmId alarmId) { log.trace("Executing findAlarmById [{}]", alarmId); validateId(alarmId, "Incorrect alarmId " + alarmId); + return alarmDao.findAlarmById(tenantId, alarmId.getId()); + } + + @Override + public ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId) { + log.trace("Executing findAlarmByIdAsync [{}]", alarmId); + validateId(alarmId, "Incorrect alarmId " + alarmId); return alarmDao.findAlarmByIdAsync(tenantId, alarmId.getId()); } From 74f12faa72a097c0f02f78020a92dd5e62d32592 Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Fri, 4 Nov 2022 17:18:28 +0200 Subject: [PATCH 036/226] removed unused import --- .../service/telemetry/DefaultAlarmSubscriptionService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java index 64d4630314..956dde8024 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -52,7 +52,6 @@ import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import java.util.Collection; import java.util.Optional; -import java.util.concurrent.ExecutionException; /** * Created by ashvayka on 27.03.18. From 5776f5b39bc0ba1d1fe535375a91e6383683a37a Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 27 Oct 2022 15:22:45 +0300 Subject: [PATCH 037/226] js-executor - improved lifecycle events and added kill for crash event --- msa/js-executor/queue/kafkaTemplate.ts | 4 ++++ msa/js-executor/server.ts | 22 +++++++++++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/msa/js-executor/queue/kafkaTemplate.ts b/msa/js-executor/queue/kafkaTemplate.ts index 7c34d99889..6cb8281837 100644 --- a/msa/js-executor/queue/kafkaTemplate.ts +++ b/msa/js-executor/queue/kafkaTemplate.ts @@ -30,6 +30,8 @@ import { TopicMessages } from 'kafkajs'; +import process, { kill, exit } from 'process'; + export class KafkaTemplate implements IQueue { private logger = _logger(`kafkaTemplate`); @@ -122,6 +124,7 @@ export class KafkaTemplate implements IQueue { this.logger.error(`Got consumer CRASH event, should restart: ${e.payload.restart}`); if (!e.payload.restart) { this.logger.error('Going to exit due to not retryable error!'); + kill(process.pid, 'SIGTERM'); //sending signal to myself process to trigger the handler await this.destroy(); } }); @@ -253,6 +256,7 @@ export class KafkaTemplate implements IQueue { } } this.logger.info('Kafka resources stopped.'); + exit(0); //same as in version before } private async disconnectProducer(): Promise { diff --git a/msa/js-executor/server.ts b/msa/js-executor/server.ts index 035bf77183..a64dcc2c98 100644 --- a/msa/js-executor/server.ts +++ b/msa/js-executor/server.ts @@ -81,15 +81,19 @@ process.on('exit', (code: number) => { async function exit(status: number) { logger.info('Exiting with status: %d ...', status); - if (httpServer) { - const _httpServer = httpServer; - httpServer = null; - await _httpServer.stop(); - } - if (queues) { - const _queues = queues; - queues = null; - await _queues.destroy(); + try { + if (httpServer) { + const _httpServer = httpServer; + httpServer = null; + await _httpServer.stop(); + } + if (queues) { + const _queues = queues; + queues = null; + await _queues.destroy(); + } + } catch (e) { + logger.error('Error on exit'); } process.exit(status); } From 62d6ffd7157470ae09294c504aaaed696148757b Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 4 Nov 2022 18:28:54 +0200 Subject: [PATCH 038/226] Moved black-box tests to TestNg, added TestRestClient --- application/src/main/resources/logback.xml | 3 +- msa/black-box-tests/pom.xml | 29 +- .../server/msa/AbstractContainerTest.java | 110 ++------ .../server/msa/ContainerTestSuite.java | 221 ++++++++------- .../server/msa/TestProperties.java | 58 ++++ .../server/msa/TestRestClient.java | 253 ++++++++++++++++++ .../server/msa/ThingsBoardDbInstaller.java | 9 +- .../msa/connectivity/HttpClientTest.java | 114 +++----- .../msa/connectivity/MqttClientTest.java | 227 ++++++---------- .../connectivity/MqttGatewayClientTest.java | 230 +++++++--------- .../msa/prototypes/DevicePrototypes.java | 41 +++ .../src/test/resources/config.properties | 2 + 12 files changed, 750 insertions(+), 547 deletions(-) create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java create mode 100644 msa/black-box-tests/src/test/resources/config.properties diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index 941bd7d278..90ed4785df 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -25,7 +25,8 @@ - + + diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index a1c745e163..054b4ee5a8 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -57,11 +57,6 @@ httpclient test - - io.takari.junit - takari-cpsuite - test - org.springframework.boot spring-boot-starter-test @@ -72,6 +67,30 @@ junit-vintage-engine test + + org.testng + testng + 7.6.1 + test + + + org.assertj + assertj-core + 3.23.1 + test + + + io.rest-assured + rest-assured + 5.2.0 + test + + + org.hamcrest + hamcrest-all + 1.3 + test + org.awaitility awaitility diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java index 07529baead..df622a7752 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java @@ -15,13 +15,14 @@ */ package org.thingsboard.server.msa; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import io.restassured.RestAssured; +import io.restassured.filter.log.RequestLoggingFilter; +import io.restassured.filter.log.ResponseLoggingFilter; import lombok.extern.slf4j.Slf4j; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; @@ -33,97 +34,52 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.ssl.SSLContexts; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.rules.TestRule; -import org.junit.rules.TestWatcher; -import org.junit.runner.Description; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.thingsboard.rest.client.RestClient; -import org.thingsboard.server.common.data.Device; +import org.testng.annotations.AfterSuite; +import org.testng.annotations.BeforeSuite; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.msa.mapper.WsTelemetryResponse; import javax.net.ssl.SSLContext; import java.net.URI; -import java.util.List; -import java.util.Map; -import java.util.Random; +import java.util.*; @Slf4j public abstract class AbstractContainerTest { - protected static final String HTTPS_URL = "https://localhost"; - protected static final String WSS_URL = "wss://localhost"; - protected static String TB_TOKEN; - protected static RestClient restClient; - protected static long timeoutMultiplier = 1; protected ObjectMapper mapper = new ObjectMapper(); protected JsonParser jsonParser = new JsonParser(); - - @BeforeClass - public static void before() throws Exception { - restClient = new RestClient(HTTPS_URL); - restClient.getRestTemplate().setRequestFactory(getRequestFactoryForSelfSignedCert()); - + protected ContainerTestSuite containerTestSuite = ContainerTestSuite.getInstance(); + protected static TestRestClient testRestClient; + + @BeforeSuite + public void beforeSuite() { + RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); + if ("false".equals(System.getProperty("runLocal", "false"))) { + containerTestSuite.start(); + } + testRestClient = new TestRestClient(TestProperties.getBaseUrl()); if (!"kafka".equals(System.getProperty("blackBoxTests.queue", "kafka"))) { timeoutMultiplier = 10; } } - @Rule - public TestRule watcher = new TestWatcher() { - protected void starting(Description description) { - log.info("================================================="); - log.info("STARTING TEST: {}" , description.getMethodName()); - log.info("================================================="); + @AfterSuite + public void afterSuite() { + if (containerTestSuite.isActive()) { + containerTestSuite.stop(); } - - /** - * Invoked when a test succeeds - */ - protected void succeeded(Description description) { - log.info("================================================="); - log.info("SUCCEEDED TEST: {}" , description.getMethodName()); - log.info("================================================="); - } - - /** - * Invoked when a test fails - */ - protected void failed(Throwable e, Description description) { - log.info("================================================="); - log.info("FAILED TEST: {}" , description.getMethodName(), e); - log.info("================================================="); - } - }; - - protected Device createGatewayDevice() throws JsonProcessingException { - String isGateway = "{\"gateway\":true}"; - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode additionalInfo = objectMapper.readTree(isGateway); - Device gatewayDeviceTemplate = new Device(); - gatewayDeviceTemplate.setName("mqtt_gateway"); - gatewayDeviceTemplate.setType("gateway"); - gatewayDeviceTemplate.setAdditionalInfo(additionalInfo); - return restClient.saveDevice(gatewayDeviceTemplate); - } - - protected Device createDevice(String name) { - Device device = new Device(); - device.setName(name + StringUtils.randomAlphanumeric(7)); - device.setType("DEFAULT"); - return restClient.saveDevice(device); } protected WsClient subscribeToWebSocket(DeviceId deviceId, String scope, CmdsType property) throws Exception { - WsClient wsClient = new WsClient(new URI(WSS_URL + "/api/ws/plugins/telemetry?token=" + restClient.getToken()), timeoutMultiplier); - SSLContextBuilder builder = SSLContexts.custom(); - builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true); - wsClient.setSocketFactory(builder.build().getSocketFactory()); + String webSocketUrl = TestProperties.getWebSocketUrl(); + WsClient wsClient = new WsClient(new URI(webSocketUrl + "/api/ws/plugins/telemetry?token=" + testRestClient.getToken()), timeoutMultiplier); + if (webSocketUrl.matches("^(wss)://.*$")) { + SSLContextBuilder builder = SSLContexts.custom(); + builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true); + wsClient.setSocketFactory(builder.build().getSocketFactory()); + } wsClient.connectBlocking(); JsonObject cmdsObject = new JsonObject(); @@ -150,16 +106,6 @@ public abstract class AbstractContainerTest { .build(); } - protected boolean verify(WsTelemetryResponse wsTelemetryResponse, String key, Long expectedTs, String expectedValue) { - List list = wsTelemetryResponse.getDataValuesByKey(key); - return expectedTs.equals(list.get(0)) && expectedValue.equals(list.get(1)); - } - - protected boolean verify(WsTelemetryResponse wsTelemetryResponse, String key, String expectedValue) { - List list = wsTelemetryResponse.getDataValuesByKey(key); - return expectedValue.equals(list.get(1)); - } - protected JsonObject createGatewayConnectPayload(String deviceName){ JsonObject payload = new JsonObject(); payload.addProperty("device", deviceName); @@ -216,7 +162,7 @@ public abstract class AbstractContainerTest { } } - private static HttpComponentsClientHttpRequestFactory getRequestFactoryForSelfSignedCert() throws Exception { + public static HttpComponentsClientHttpRequestFactory getRequestFactoryForSelfSignedCert() throws Exception { SSLContextBuilder builder = SSLContexts.custom(); builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true); SSLContext sslContext = builder.build(); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java index 47acbde16e..c32a6bdbe6 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java @@ -17,9 +17,6 @@ package org.thingsboard.server.msa; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; -import org.junit.ClassRule; -import org.junit.extensions.cpsuite.ClasspathSuite; -import org.junit.runner.RunWith; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.thingsboard.server.common.data.StringUtils; @@ -41,10 +38,8 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.fail; +import static org.testng.Assert.fail; -@RunWith(ClasspathSuite.class) -@ClasspathSuite.ClassnameFilters({"org.thingsboard.server.msa.*Test"}) @Slf4j public class ContainerTestSuite { final static boolean IS_REDIS_CLUSTER = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisCluster")); @@ -57,107 +52,133 @@ public class ContainerTestSuite { private static final String TB_JS_EXECUTOR_LOG_REGEXP = ".*template started.*"; private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(400); - private static DockerComposeContainer testContainer; - - @ClassRule - public static ThingsBoardDbInstaller installTb = new ThingsBoardDbInstaller(); - - @ClassRule - public static DockerComposeContainer getTestContainer() { - if (testContainer == null) { - log.info("System property of blackBoxTests.redisCluster is {}", IS_REDIS_CLUSTER); - log.info("System property of blackBoxTests.hybridMode is {}", IS_HYBRID_MODE); - boolean skipTailChildContainers = Boolean.valueOf(System.getProperty("blackBoxTests.skipTailChildContainers")); - try { - final String targetDir = FileUtils.getTempDirectoryPath() + "/" + "ContainerTestSuite-" + UUID.randomUUID() + "/"; - log.info("targetDir {}", targetDir); - FileUtils.copyDirectory(new File(SOURCE_DIR), new File(targetDir)); - replaceInFile(targetDir + "docker-compose.yml", " container_name: \"${LOAD_BALANCER_NAME}\"", "", "container_name"); - - FileUtils.copyDirectory(new File("src/test/resources"), new File(targetDir)); - - class DockerComposeContainerImpl> extends DockerComposeContainer { - public DockerComposeContainerImpl(List composeFiles) { - super(composeFiles); - } - - @Override - public void stop() { - super.stop(); - tryDeleteDir(targetDir); - } - } + private DockerComposeContainer testContainer; + private ThingsBoardDbInstaller installTb; + public boolean isActive; + + private static ContainerTestSuite containerTestSuite; + + public boolean isActive() { + return isActive; + } + + public void setActive(boolean active) { + isActive = active; + } + + private ContainerTestSuite() { + } + + public static ContainerTestSuite getInstance() { + if (containerTestSuite == null) { + containerTestSuite = new ContainerTestSuite(); + } + return containerTestSuite; + } + + public void start() { + installTb = new ThingsBoardDbInstaller(); + installTb.createVolumes(); + log.info("System property of blackBoxTests.redisCluster is {}", IS_REDIS_CLUSTER); + log.info("System property of blackBoxTests.hybridMode is {}", IS_HYBRID_MODE); + boolean skipTailChildContainers = Boolean.valueOf(System.getProperty("blackBoxTests.skipTailChildContainers")); + try { + final String targetDir = FileUtils.getTempDirectoryPath() + "/" + "ContainerTestSuite-" + UUID.randomUUID() + "/"; + log.info("targetDir {}", targetDir); + FileUtils.copyDirectory(new File(SOURCE_DIR), new File(targetDir)); + replaceInFile(targetDir + "docker-compose.yml", " container_name: \"${LOAD_BALANCER_NAME}\"", "", "container_name"); - List composeFiles = new ArrayList<>(Arrays.asList( - new File(targetDir + "docker-compose.yml"), - new File(targetDir + "docker-compose.volumes.yml"), - new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid.yml" : "docker-compose.postgres.yml")), - new File(targetDir + "docker-compose.postgres.volumes.yml"), - new File(targetDir + "docker-compose." + QUEUE_TYPE + ".yml"), - new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.yml" : "docker-compose.redis.yml")), - new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.volumes.yml" : "docker-compose.redis.volumes.yml")) - )); - - Map queueEnv = new HashMap<>(); - queueEnv.put("TB_QUEUE_TYPE", QUEUE_TYPE); - switch (QUEUE_TYPE) { - case "kafka": - composeFiles.add(new File(targetDir + "docker-compose.kafka.yml")); - break; - case "aws-sqs": - replaceInFile(targetDir, "queue-aws-sqs.env", - Map.of("YOUR_KEY", getSysProp("blackBoxTests.awsKey"), - "YOUR_SECRET", getSysProp("blackBoxTests.awsSecret"), - "YOUR_REGION", getSysProp("blackBoxTests.awsRegion"))); - break; - case "rabbitmq": - composeFiles.add(new File(targetDir + "docker-compose.rabbitmq-server.yml")); - replaceInFile(targetDir, "queue-rabbitmq.env", - Map.of("localhost", "rabbitmq")); - break; - case "service-bus": - replaceInFile(targetDir, "queue-service-bus.env", - Map.of("YOUR_NAMESPACE_NAME", getSysProp("blackBoxTests.serviceBusNamespace"), - "YOUR_SAS_KEY_NAME", getSysProp("blackBoxTests.serviceBusSASPolicy"))); - replaceInFile(targetDir, "queue-service-bus.env", - Map.of("YOUR_SAS_KEY", getSysProp("blackBoxTests.serviceBusPrimaryKey"))); - break; - case "pubsub": - replaceInFile(targetDir, "queue-pubsub.env", - Map.of("YOUR_PROJECT_ID", getSysProp("blackBoxTests.pubSubProjectId"), - "YOUR_SERVICE_ACCOUNT", getSysProp("blackBoxTests.pubSubServiceAccount"))); - break; - default: - throw new RuntimeException("Unsupported queue type: " + QUEUE_TYPE); + FileUtils.copyDirectory(new File("src/test/resources"), new File(targetDir)); + + class DockerComposeContainerImpl> extends DockerComposeContainer { + public DockerComposeContainerImpl(List composeFiles) { + super(composeFiles); } - if (IS_HYBRID_MODE) { - composeFiles.add(new File(targetDir + "docker-compose.cassandra.volumes.yml")); + @Override + public void stop() { + super.stop(); + tryDeleteDir(targetDir); } + } - testContainer = new DockerComposeContainerImpl<>(composeFiles) - .withPull(false) - .withLocalCompose(true) - .withTailChildContainers(!skipTailChildContainers) - .withEnv(installTb.getEnv()) - .withEnv(queueEnv) - .withEnv("LOAD_BALANCER_NAME", "") - .withExposedService("haproxy", 80, Wait.forHttp("/swagger-ui.html").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("tb-core1", Wait.forLogMessage(TB_CORE_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("tb-core2", Wait.forLogMessage(TB_CORE_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("tb-http-transport1", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("tb-http-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("tb-mqtt-transport1", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("tb-mqtt-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("tb-vc-executor1", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("tb-vc-executor2", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("tb-js-executor", Wait.forLogMessage(TB_JS_EXECUTOR_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)); - } catch (Exception e) { - log.error("Failed to create test container", e); - fail("Failed to create test container"); + List composeFiles = new ArrayList<>(Arrays.asList( + new File(targetDir + "docker-compose.yml"), + new File(targetDir + "docker-compose.volumes.yml"), + new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid.yml" : "docker-compose.postgres.yml")), + new File(targetDir + "docker-compose.postgres.volumes.yml"), + new File(targetDir + "docker-compose." + QUEUE_TYPE + ".yml"), + new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.yml" : "docker-compose.redis.yml")), + new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.volumes.yml" : "docker-compose.redis.volumes.yml")) + )); + + Map queueEnv = new HashMap<>(); + queueEnv.put("TB_QUEUE_TYPE", QUEUE_TYPE); + switch (QUEUE_TYPE) { + case "kafka": + composeFiles.add(new File(targetDir + "docker-compose.kafka.yml")); + break; + case "aws-sqs": + replaceInFile(targetDir, "queue-aws-sqs.env", + Map.of("YOUR_KEY", getSysProp("blackBoxTests.awsKey"), + "YOUR_SECRET", getSysProp("blackBoxTests.awsSecret"), + "YOUR_REGION", getSysProp("blackBoxTests.awsRegion"))); + break; + case "rabbitmq": + composeFiles.add(new File(targetDir + "docker-compose.rabbitmq-server.yml")); + replaceInFile(targetDir, "queue-rabbitmq.env", + Map.of("localhost", "rabbitmq")); + break; + case "service-bus": + replaceInFile(targetDir, "queue-service-bus.env", + Map.of("YOUR_NAMESPACE_NAME", getSysProp("blackBoxTests.serviceBusNamespace"), + "YOUR_SAS_KEY_NAME", getSysProp("blackBoxTests.serviceBusSASPolicy"))); + replaceInFile(targetDir, "queue-service-bus.env", + Map.of("YOUR_SAS_KEY", getSysProp("blackBoxTests.serviceBusPrimaryKey"))); + break; + case "pubsub": + replaceInFile(targetDir, "queue-pubsub.env", + Map.of("YOUR_PROJECT_ID", getSysProp("blackBoxTests.pubSubProjectId"), + "YOUR_SERVICE_ACCOUNT", getSysProp("blackBoxTests.pubSubServiceAccount"))); + break; + default: + throw new RuntimeException("Unsupported queue type: " + QUEUE_TYPE); } + + if (IS_HYBRID_MODE) { + composeFiles.add(new File(targetDir + "docker-compose.cassandra.volumes.yml")); + } + + testContainer = new DockerComposeContainerImpl<>(composeFiles) + .withPull(false) + .withLocalCompose(true) + .withTailChildContainers(!skipTailChildContainers) + .withEnv(installTb.getEnv()) + .withEnv(queueEnv) + .withEnv("LOAD_BALANCER_NAME", "") + .withExposedService("haproxy", 80, Wait.forHttp("/swagger-ui.html").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("tb-core1", Wait.forLogMessage(TB_CORE_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("tb-core2", Wait.forLogMessage(TB_CORE_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("tb-http-transport1", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("tb-http-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("tb-mqtt-transport1", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("tb-mqtt-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("tb-vc-executor1", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("tb-vc-executor2", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("tb-js-executor", Wait.forLogMessage(TB_JS_EXECUTOR_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)); + testContainer.start(); + setActive(true); + } catch (Exception e) { + log.error("Failed to create test container", e); + fail("Failed to create test container"); + } + } + public void stop() { + if (isActive) { + testContainer.stop(); + installTb.savaLogsAndRemoveVolumes(); + setActive(false); } - return testContainer; } private static void replaceInFile(String targetDir, String fileName, Map replacements) throws IOException { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java new file mode 100644 index 0000000000..f50caa1aee --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2022 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.msa; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public class TestProperties { + public static final String HTTPS_URL = "https://localhost"; + + protected static final String WSS_URL = "wss://localhost"; + + public static final ContainerTestSuite instance = ContainerTestSuite.getInstance(); + + public static String getBaseUrl(){ + if (instance.isActive()) { + return HTTPS_URL; + } + return getProperty("tb.baseUrl"); + } + + public static String getWebSocketUrl(){ + if (instance.isActive()) { + return WSS_URL; + } + return getProperty("tb.baseUrl"); + } + + private static String getProperty(String propertyName) { + + try (InputStream input = TestProperties.class.getClassLoader().getResourceAsStream("config.properties")) { + Properties prop = new Properties(); + prop.load(input); + return prop.getProperty(propertyName); + + } catch (IOException ex) { + ex.printStackTrace(); + } + return null; + + } + +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java new file mode 100644 index 0000000000..eaf5332323 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -0,0 +1,253 @@ +/** + * Copyright © 2016-2022 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.msa; + +import com.fasterxml.jackson.databind.JsonNode; +import io.restassured.common.mapper.TypeRef; +import io.restassured.http.ContentType; +import io.restassured.path.json.JsonPath; +import io.restassured.response.ValidatableResponse; +import io.restassured.specification.RequestSpecification; +import org.springframework.http.HttpStatus; +import org.thingsboard.rest.client.RestClient; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.security.DeviceCredentials; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.core.AnyOf.anyOf; +import static org.thingsboard.server.common.data.StringUtils.isEmpty; +import static org.thingsboard.server.msa.AbstractContainerTest.getRequestFactoryForSelfSignedCert; + +public class TestRestClient { + private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization"; + private final String baseURL; + private String token; + private String refreshToken; + private RequestSpecification spec; + protected static final String ACTIVATE_TOKEN_REGEX = "/api/noauth/activate?activateToken="; + + public TestRestClient(String url) { + baseURL = url; + spec = given().baseUri(baseURL).contentType(ContentType.JSON); + if (url.matches("^(https)://.*$")) { + spec.relaxedHTTPSValidation(); + } + } + + public void login(String username, String password) throws Exception { + Map loginRequest = new HashMap<>(); + loginRequest.put("username", username); + loginRequest.put("password", password); + + JsonPath jsonPath = given().relaxedHTTPSValidation().body(loginRequest).post(baseURL + "/api/auth/login") + .getBody().jsonPath(); + token = jsonPath.get("token"); + refreshToken = jsonPath.get("refreshToken"); + spec.header(JWT_TOKEN_HEADER_PARAM, "Bearer " + token) + .contentType(ContentType.JSON); + } + + public Device postDevice(String accessToken, Device device) { + return given().spec(spec).body(device) + .pathParams("accessToken", accessToken) + .post("/api/device?accessToken={accessToken}") + .then() + .statusCode(HttpStatus.OK.value()) + .extract() + .as(Device.class); + } + + public ValidatableResponse getDeviceById(DeviceId deviceId, int statusCode) { + return given().spec(spec) + .pathParams("deviceId", deviceId.getId()) + .get("/api/device/{deviceId}") + .then() + .statusCode(statusCode); + } + public Device getDeviceById(DeviceId deviceId) { + return getDeviceById(deviceId, HttpStatus.OK.value()) + .extract() + .as(Device.class); + } + public DeviceCredentials getDeviceCredentialsByDeviceId(DeviceId deviceId) { + return given().spec(spec).get("/api/device/{deviceId}/credentials", deviceId.getId()) + .then() + .assertThat() + .statusCode(HttpStatus.OK.value()) + .extract() + .as(DeviceCredentials.class); + } + + public ValidatableResponse postTelemetry(String credentialsId, JsonNode telemetry) { + return given().spec(spec).body(telemetry) + .post("/api/v1/{credentialsId}/telemetry", credentialsId) + .then() + .statusCode(HttpStatus.OK.value()); + } + + public ValidatableResponse deleteDevice(DeviceId deviceId) { + return given().spec(spec) + .delete("/api/device/{deviceId}", deviceId.getId()) + .then() + .statusCode(HttpStatus.OK.value()); + } + public ValidatableResponse deleteDeviceIfExists(DeviceId deviceId) { + return given().spec(spec) + .delete("/api/device/{deviceId}", deviceId.getId()) + .then() + .statusCode(anyOf(is(HttpStatus.OK.value()),is(HttpStatus.NOT_FOUND.value()))); + } + + public ValidatableResponse postTelemetryAttribute(String entityType, DeviceId deviceId, String scope, JsonNode attribute) { + return given().spec(spec).body(attribute) + .post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityType, deviceId.getId(), scope) + .then() + .statusCode(HttpStatus.OK.value()); + } + + public ValidatableResponse postAttribute(String accessToken, JsonNode attribute) { + return given().spec(spec).body(attribute) + .post("/api/v1/{accessToken}/attributes/", accessToken) + .then() + .statusCode(HttpStatus.OK.value()); + } + + public JsonNode getAttributes(String accessToken, String clientKeys, String sharedKeys) { + return given().spec(spec) + .queryParam("clientKeys", clientKeys) + .queryParam("sharedKeys", sharedKeys) + .get("/api/v1/{accessToken}/attributes", accessToken) + .then() + .statusCode(HttpStatus.OK.value()) + .extract() + .as(JsonNode.class); + } + + public PageData getRuleChains(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return given().spec(spec).queryParams(params) + .get("/api/ruleChains") + .then() + .statusCode(HttpStatus.OK.value()) + .extract() + .as(new TypeRef>() {}); + } + + public RuleChain postRootRuleChain(RuleChain ruleChain) { + return given().spec(spec) + .body(ruleChain) + .post("/api/ruleChain") + .then() + .statusCode(HttpStatus.OK.value()) + .extract() + .as(RuleChain.class); + } + + public RuleChainMetaData postRuleChainMetadata(RuleChainMetaData ruleChainMetaData) { + return given().spec(spec) + .body(ruleChainMetaData) + .post("/api/ruleChain/metadata") + .then() + .statusCode(HttpStatus.OK.value()) + .extract() + .as(RuleChainMetaData.class); + } + + public void setRootRuleChain(RuleChainId ruleChainId) { + given().spec(spec) + .post("/api/ruleChain/{ruleChainId}/root", ruleChainId.getId()) + .then() + .statusCode(HttpStatus.OK.value()); + } + + public void deleteRuleChain(RuleChainId ruleChainId) { + given().spec(spec) + .delete("/api/ruleChain/{ruleChainId}", ruleChainId.getId()) + .then() + .statusCode(HttpStatus.OK.value()); + } + + private String getUrlParams(PageLink pageLink) { + String urlParams = "pageSize={pageSize}&page={page}"; + if (!isEmpty(pageLink.getTextSearch())) { + urlParams += "&textSearch={textSearch}"; + } + if (pageLink.getSortOrder() != null) { + urlParams += "&sortProperty={sortProperty}&sortOrder={sortOrder}"; + } + return urlParams; + } + + private void addPageLinkToParam(Map params, PageLink pageLink) { + params.put("pageSize", String.valueOf(pageLink.getPageSize())); + params.put("page", String.valueOf(pageLink.getPage())); + if (!isEmpty(pageLink.getTextSearch())) { + params.put("textSearch", pageLink.getTextSearch()); + } + if (pageLink.getSortOrder() != null) { + params.put("sortProperty", pageLink.getSortOrder().getProperty()); + params.put("sortOrder", pageLink.getSortOrder().getDirection().name()); + } + } + + public List findRelationByFrom(EntityId fromId, RelationTypeGroup relationTypeGroup) { + Map params = new HashMap<>(); + params.put("fromId", fromId.getId().toString()); + params.put("fromType", fromId.getEntityType().name()); + params.put("relationTypeGroup", relationTypeGroup.name()); + + return given().spec(spec) + .pathParams(params) + .get("/api/relations?fromId={fromId}&fromType={fromType}&relationTypeGroup={relationTypeGroup}") + .then() + .statusCode(HttpStatus.OK.value()) + .extract() + .as(new TypeRef>() {}); + } + + public JsonNode postServerSideRpc(DeviceId deviceId, JsonNode serverRpcPayload) { + return given().spec(spec) + .body(serverRpcPayload) + .post("/api/rpc/twoway/{deviceId}", deviceId.getId()) + .then() + .statusCode(HttpStatus.OK.value()) + .extract() + .as(JsonNode.class); + } + + public String getToken() { + return token; + } + + public String getRefreshToken() { + return refreshToken; + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java index 979fbb187d..ed606cd468 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java @@ -16,7 +16,6 @@ package org.thingsboard.server.msa; import lombok.extern.slf4j.Slf4j; -import org.junit.rules.ExternalResource; import org.testcontainers.utility.Base58; import org.thingsboard.server.common.data.StringUtils; @@ -30,7 +29,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; @Slf4j -public class ThingsBoardDbInstaller extends ExternalResource { +public class ThingsBoardDbInstaller { final static boolean IS_REDIS_CLUSTER = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisCluster")); final static boolean IS_HYBRID_MODE = Boolean.parseBoolean(System.getProperty("blackBoxTests.hybridMode")); @@ -129,8 +128,7 @@ public class ThingsBoardDbInstaller extends ExternalResource { return env; } - @Override - protected void before() throws Throwable { + public void createVolumes() { try { dockerCompose.withCommand("volume create " + postgresDataVolume); @@ -192,8 +190,7 @@ public class ThingsBoardDbInstaller extends ExternalResource { } } - @Override - protected void after() { + public void savaLogsAndRemoveVolumes() { copyLogs(tbLogVolume, "./target/tb-logs/"); copyLogs(tbCoapTransportLogVolume, "./target/tb-coap-transport-logs/"); copyLogs(tbLwm2mTransportLogVolume, "./target/tb-lwm2m-transport-logs/"); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java index f81d03b394..6f24f00854 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java @@ -16,10 +16,9 @@ package org.thingsboard.server.msa.connectivity; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.Sets; -import org.junit.Assert; -import org.junit.Test; -import org.springframework.http.ResponseEntity; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.msa.AbstractContainerTest; @@ -27,98 +26,69 @@ import org.thingsboard.server.msa.WsClient; import org.thingsboard.server.msa.mapper.WsTelemetryResponse; -import java.util.Optional; +import java.util.Arrays; import java.util.concurrent.TimeUnit; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.thingsboard.server.common.data.DataConstants.DEVICE; -import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.DataConstants.*; +import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; public class HttpClientTest extends AbstractContainerTest { + private Device device; + @BeforeMethod + public void setUp() throws Exception { + testRestClient.login("tenant@thingsboard.org", "tenant"); + device = testRestClient.postDevice("", defaultDevicePrototype("http_")); + } + + @AfterMethod + public void tearDown() { + testRestClient.deleteDeviceIfExists(device.getId()); + } @Test public void telemetryUpload() throws Exception { - restClient.login("tenant@thingsboard.org", "tenant"); - - Device device = createDevice("http_"); - DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); + DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS); - ResponseEntity deviceTelemetryResponse = restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/v1/{credentialsId}/telemetry", - mapper.readTree(createPayload().toString()), - ResponseEntity.class, - deviceCredentials.getCredentialsId()); - Assert.assertTrue(deviceTelemetryResponse.getStatusCode().is2xxSuccessful()); + testRestClient.postTelemetry(deviceCredentials.getCredentialsId(), mapper.readTree(createPayload().toString())); + WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage(); wsClient.closeBlocking(); - Assert.assertEquals(Sets.newHashSet("booleanKey", "stringKey", "doubleKey", "longKey"), - actualLatestTelemetry.getLatestValues().keySet()); - - Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", Boolean.TRUE.toString())); - Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", "value1")); - Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0))); - Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73))); + assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("booleanKey", "stringKey", "doubleKey", "longKey")); - restClient.deleteDevice(device.getId()); + assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString()); + assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1"); + assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0)); + assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73)); } @Test public void getAttributes() throws Exception { - restClient.login("tenant@thingsboard.org", "tenant"); - TB_TOKEN = restClient.getToken(); - - Device device = createDevice("test"); - String accessToken = restClient.getDeviceCredentialsByDeviceId(device.getId()).get().getCredentialsId(); - assertNotNull(accessToken); + String accessToken = testRestClient.getDeviceCredentialsByDeviceId(device.getId()).getCredentialsId(); + assertThat(accessToken).isNotNull(); - ResponseEntity deviceSharedAttributes = restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/plugins/telemetry/" + DEVICE + "/" + device.getId().toString() + "/attributes/" + SHARED_SCOPE, mapper.readTree(createPayload().toString()), - ResponseEntity.class, - accessToken); + JsonNode sharedAattribute = mapper.readTree(createPayload().toString()); + testRestClient.postTelemetryAttribute(DEVICE, device.getId(), SHARED_SCOPE, sharedAattribute); - Assert.assertTrue(deviceSharedAttributes.getStatusCode().is2xxSuccessful()); - - ResponseEntity deviceClientsAttributes = restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/v1/" + accessToken + "/attributes/", mapper.readTree(createPayload().toString()), - ResponseEntity.class, - accessToken); - - Assert.assertTrue(deviceClientsAttributes.getStatusCode().is2xxSuccessful()); + JsonNode clientAttribute = mapper.readTree(createPayload().toString()); + testRestClient.postAttribute(accessToken, clientAttribute); TimeUnit.SECONDS.sleep(3 * timeoutMultiplier); - @SuppressWarnings("deprecation") - Optional allOptional = restClient.getAttributes(accessToken, null, null); - assertTrue(allOptional.isPresent()); - - - JsonNode all = allOptional.get(); - assertEquals(2, all.size()); - assertEquals(mapper.readTree(createPayload().toString()), all.get("shared")); - assertEquals(mapper.readTree(createPayload().toString()), all.get("client")); - - @SuppressWarnings("deprecation") - Optional sharedOptional = restClient.getAttributes(accessToken, null, "stringKey"); - assertTrue(sharedOptional.isPresent()); - - JsonNode shared = sharedOptional.get(); - assertEquals(shared.get("shared").get("stringKey"), mapper.readTree(createPayload().get("stringKey").toString())); - assertFalse(shared.has("client")); + JsonNode attributes = testRestClient.getAttributes(accessToken, null, null); + assertThat(attributes.get("shared")).isEqualTo(sharedAattribute); + assertThat(attributes.get("client")).isEqualTo(clientAttribute); - @SuppressWarnings("deprecation") - Optional clientOptional = restClient.getAttributes(accessToken, "longKey,stringKey", null); - assertTrue(clientOptional.isPresent()); + JsonNode attributes2 = testRestClient.getAttributes(accessToken, null, "stringKey"); + assertThat(attributes2.get("shared").get("stringKey")).isEqualTo(sharedAattribute.get("stringKey")); + assertThat(attributes2.has("client")).isFalse(); - JsonNode client = clientOptional.get(); - assertFalse(client.has("shared")); - assertEquals(mapper.readTree(createPayload().get("longKey").toString()), client.get("client").get("longKey")); - assertEquals(client.get("client").get("stringKey"), mapper.readTree(createPayload().get("stringKey").toString())); + JsonNode attributes3 = testRestClient.getAttributes(accessToken, "longKey,stringKey", null); - restClient.deleteDevice(device.getId()); + assertThat(attributes3.has("shared")).isFalse(); + assertThat(attributes3.get("client").get("longKey")).isEqualTo(clientAttribute.get("longKey")); + assertThat(attributes3.get("client").get("stringKey")).isEqualTo(clientAttribute.get("stringKey")); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java index ce36f08a64..7e7ebf07ab 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.msa.connectivity; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.Sets; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; @@ -26,19 +25,19 @@ import io.netty.buffer.Unpooled; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.junit.Assert; -import org.junit.Test; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttHandler; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; @@ -61,14 +60,30 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; +import static org.testng.Assert.fail; +import static org.thingsboard.server.common.data.DataConstants.DEVICE; +import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; +import static org.thingsboard.server.msa.TestProperties.HTTPS_URL; +import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; + @Slf4j public class MqttClientTest extends AbstractContainerTest { + private Device device; + @BeforeMethod + public void setUp() throws Exception { + testRestClient.login("tenant@thingsboard.org", "tenant"); + device = testRestClient.postDevice("", defaultDevicePrototype("http_")); + } + + @AfterMethod + public void tearDown() { + testRestClient.deleteDeviceIfExists(device.getId()); + } @Test public void telemetryUpload() throws Exception { - restClient.login("tenant@thingsboard.org", "tenant"); - Device device = createDevice("mqtt_"); - DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); + DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS); MqttClient mqttClient = getMqttClient(deviceCredentials, null); @@ -77,25 +92,19 @@ public class MqttClientTest extends AbstractContainerTest { log.info("Received telemetry: {}", actualLatestTelemetry); wsClient.closeBlocking(); - Assert.assertEquals(4, actualLatestTelemetry.getData().size()); - Assert.assertEquals(Sets.newHashSet("booleanKey", "stringKey", "doubleKey", "longKey"), - actualLatestTelemetry.getLatestValues().keySet()); + assertThat(actualLatestTelemetry.getData()).hasSize(4); + assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("booleanKey", "stringKey", "doubleKey", "longKey")); - Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", Boolean.TRUE.toString())); - Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", "value1")); - Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0))); - Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73))); - - restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId()); + assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString()); + assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1"); + assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0)); + assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73)); } @Test public void telemetryUploadWithTs() throws Exception { long ts = 1451649600512L; - - restClient.login("tenant@thingsboard.org", "tenant"); - Device device = createDevice("mqtt_"); - DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); + DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS); MqttClient mqttClient = getMqttClient(deviceCredentials, null); @@ -104,22 +113,18 @@ public class MqttClientTest extends AbstractContainerTest { log.info("Received telemetry: {}", actualLatestTelemetry); wsClient.closeBlocking(); - Assert.assertEquals(4, actualLatestTelemetry.getData().size()); - Assert.assertEquals(getExpectedLatestValues(ts), actualLatestTelemetry.getLatestValues()); + assertThat(actualLatestTelemetry.getData()).hasSize(4); + assertThat(getExpectedLatestValues(ts)).isEqualTo(actualLatestTelemetry.getLatestValues()); - Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", ts, Boolean.TRUE.toString())); - Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", ts, "value1")); - Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", ts, Double.toString(42.0))); - Assert.assertTrue(verify(actualLatestTelemetry, "longKey", ts, Long.toString(73))); - - restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId()); + assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString()); + assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1"); + assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0)); + assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73)); } @Test public void publishAttributeUpdateToServer() throws Exception { - restClient.login("tenant@thingsboard.org", "tenant"); - Device device = createDevice("mqtt_"); - DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); + DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); WsClient wsClient = subscribeToWebSocket(device.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS); MqttMessageListener listener = new MqttMessageListener(); @@ -134,23 +139,18 @@ public class MqttClientTest extends AbstractContainerTest { log.info("Received telemetry: {}", actualLatestTelemetry); wsClient.closeBlocking(); - Assert.assertEquals(4, actualLatestTelemetry.getData().size()); - Assert.assertEquals(Sets.newHashSet("attr1", "attr2", "attr3", "attr4"), - actualLatestTelemetry.getLatestValues().keySet()); - - Assert.assertTrue(verify(actualLatestTelemetry, "attr1", "value1")); - Assert.assertTrue(verify(actualLatestTelemetry, "attr2", Boolean.TRUE.toString())); - Assert.assertTrue(verify(actualLatestTelemetry, "attr3", Double.toString(42.0))); - Assert.assertTrue(verify(actualLatestTelemetry, "attr4", Long.toString(73))); + assertThat(actualLatestTelemetry.getData()).hasSize(4); + assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("attr1", "attr2", "attr3", "attr4")); - restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId()); + assertThat(actualLatestTelemetry.getDataValuesByKey("attr1").get(1)).isEqualTo("value1"); + assertThat(actualLatestTelemetry.getDataValuesByKey("attr2").get(1)).isEqualTo(Boolean.TRUE.toString()); + assertThat(actualLatestTelemetry.getDataValuesByKey("attr3").get(1)).isEqualTo(Double.toString(42.0)); + assertThat(actualLatestTelemetry.getDataValuesByKey("attr4").get(1)).isEqualTo(Long.toString(73)); } @Test public void requestAttributeValuesFromServer() throws Exception { - restClient.login("tenant@thingsboard.org", "tenant"); - Device device = createDevice("mqtt_"); - DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); + DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); WsClient wsClient = subscribeToWebSocket(device.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS); MqttMessageListener listener = new MqttMessageListener(); @@ -166,21 +166,16 @@ public class MqttClientTest extends AbstractContainerTest { log.info("Received ws telemetry: {}", actualLatestTelemetry); wsClient.closeBlocking(); - Assert.assertEquals(1, actualLatestTelemetry.getData().size()); - Assert.assertEquals(Sets.newHashSet("clientAttr"), - actualLatestTelemetry.getLatestValues().keySet()); - - Assert.assertTrue(verify(actualLatestTelemetry, "clientAttr", clientAttributeValue)); + assertThat(actualLatestTelemetry.getData()).hasSize(1); + assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnly("clientAttr"); + assertThat(actualLatestTelemetry.getDataValuesByKey("clientAttr").get(1)).isEqualTo(clientAttributeValue); // Add a new shared attribute JsonObject sharedAttributes = new JsonObject(); String sharedAttributeValue = StringUtils.randomAlphanumeric(8); sharedAttributes.addProperty("sharedAttr", sharedAttributeValue); - ResponseEntity sharedAttributesResponse = restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE", - mapper.readTree(sharedAttributes.toString()), ResponseEntity.class, - device.getId()); - Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful()); + JsonNode sharedAttribute = mapper.readTree(sharedAttributes.toString()); + testRestClient.postTelemetryAttribute(DataConstants.DEVICE, device.getId(), SHARED_SCOPE, sharedAttribute); // Subscribe to attributes response mqttClient.on("v1/devices/me/attributes/response/+", listener, MqttQoS.AT_LEAST_ONCE).get(); @@ -197,20 +192,16 @@ public class MqttClientTest extends AbstractContainerTest { AttributesResponse attributes = mapper.readValue(Objects.requireNonNull(event).getMessage(), AttributesResponse.class); log.info("Received telemetry: {}", attributes); - Assert.assertEquals(1, attributes.getClient().size()); - Assert.assertEquals(clientAttributeValue, attributes.getClient().get("clientAttr")); - - Assert.assertEquals(1, attributes.getShared().size()); - Assert.assertEquals(sharedAttributeValue, attributes.getShared().get("sharedAttr")); + assertThat(attributes.getClient()).hasSize(1); + assertThat(attributes.getClient().get("clientAttr")).isEqualTo(clientAttributeValue); - restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId()); + assertThat(attributes.getShared()).hasSize(1); + assertThat(attributes.getShared().get("sharedAttr")).isEqualTo(sharedAttributeValue); } @Test public void subscribeToAttributeUpdatesFromServer() throws Exception { - restClient.login("tenant@thingsboard.org", "tenant"); - Device device = createDevice("mqtt_"); - DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); + DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); MqttMessageListener listener = new MqttMessageListener(); MqttClient mqttClient = getMqttClient(deviceCredentials, listener); @@ -225,38 +216,28 @@ public class MqttClientTest extends AbstractContainerTest { JsonObject sharedAttributes = new JsonObject(); String sharedAttributeValue = StringUtils.randomAlphanumeric(8); sharedAttributes.addProperty(sharedAttributeName, sharedAttributeValue); - ResponseEntity sharedAttributesResponse = restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE", - mapper.readTree(sharedAttributes.toString()), ResponseEntity.class, - device.getId()); - Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful()); + JsonNode sharedAttribute = mapper.readTree(sharedAttributes.toString()); + + testRestClient.postTelemetryAttribute(DataConstants.DEVICE, device.getId(), SHARED_SCOPE, sharedAttribute); MqttEvent event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); - Assert.assertEquals(sharedAttributeValue, - mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText()); + assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText()) + .isEqualTo(sharedAttributeValue); // Update the shared attribute value JsonObject updatedSharedAttributes = new JsonObject(); String updatedSharedAttributeValue = StringUtils.randomAlphanumeric(8); updatedSharedAttributes.addProperty(sharedAttributeName, updatedSharedAttributeValue); - ResponseEntity updatedSharedAttributesResponse = restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE", - mapper.readTree(updatedSharedAttributes.toString()), ResponseEntity.class, - device.getId()); - Assert.assertTrue(updatedSharedAttributesResponse.getStatusCode().is2xxSuccessful()); + testRestClient.postTelemetryAttribute(DEVICE, device.getId(), SHARED_SCOPE, mapper.readTree(updatedSharedAttributes.toString())); event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); - Assert.assertEquals(updatedSharedAttributeValue, - mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText()); - - restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId()); + assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText()) + .isEqualTo(updatedSharedAttributeValue); } @Test public void serverSideRpc() throws Exception { - restClient.login("tenant@thingsboard.org", "tenant"); - Device device = createDevice("mqtt_"); - DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); + DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); MqttMessageListener listener = new MqttMessageListener(); MqttClient mqttClient = getMqttClient(deviceCredentials, listener); @@ -270,21 +251,18 @@ public class MqttClientTest extends AbstractContainerTest { serverRpcPayload.addProperty("method", "getValue"); serverRpcPayload.addProperty("params", true); ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(getClass().getSimpleName()))); - ListenableFuture future = service.submit(() -> { + ListenableFuture future = service.submit(() -> { try { - return restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/rpc/twoway/{deviceId}", - mapper.readTree(serverRpcPayload.toString()), String.class, - device.getId()); + return testRestClient.postServerSideRpc(device.getId(), mapper.readTree(serverRpcPayload.toString())); } catch (IOException e) { - return ResponseEntity.badRequest().build(); + return null; } }); // Wait for RPC call from the server and send the response MqttEvent requestFromServer = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); - Assert.assertEquals("{\"method\":\"getValue\",\"params\":true}", Objects.requireNonNull(requestFromServer).getMessage()); + assertThat(Objects.requireNonNull(requestFromServer).getMessage()).isEqualTo("{\"method\":\"getValue\",\"params\":true}"); Integer requestId = Integer.valueOf(Objects.requireNonNull(requestFromServer).getTopic().substring("v1/devices/me/rpc/request/".length())); JsonObject clientResponse = new JsonObject(); @@ -292,19 +270,14 @@ public class MqttClientTest extends AbstractContainerTest { // Send a response to the server's RPC request mqttClient.publish("v1/devices/me/rpc/response/" + requestId, Unpooled.wrappedBuffer(clientResponse.toString().getBytes())).get(); - ResponseEntity serverResponse = future.get(5 * timeoutMultiplier, TimeUnit.SECONDS); + JsonNode serverResponse = future.get(5 * timeoutMultiplier, TimeUnit.SECONDS); service.shutdownNow(); - Assert.assertTrue(serverResponse.getStatusCode().is2xxSuccessful()); - Assert.assertEquals(clientResponse.toString(), serverResponse.getBody()); - - restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId()); + assertThat(serverResponse).isEqualTo(mapper.readTree(clientResponse.toString())); } @Test public void clientSideRpc() throws Exception { - restClient.login("tenant@thingsboard.org", "tenant"); - Device device = createDevice("mqtt_"); - DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); + DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); MqttMessageListener listener = new MqttMessageListener(); MqttClient mqttClient = getMqttClient(deviceCredentials, listener); @@ -328,46 +301,33 @@ public class MqttClientTest extends AbstractContainerTest { TimeUnit.SECONDS.sleep(1 * timeoutMultiplier); MqttEvent responseFromServer = listener.getEvents().poll(1 * timeoutMultiplier, TimeUnit.SECONDS); Integer responseId = Integer.valueOf(Objects.requireNonNull(responseFromServer).getTopic().substring("v1/devices/me/rpc/response/".length())); - Assert.assertEquals(requestId, responseId); - Assert.assertEquals("requestReceived", mapper.readTree(responseFromServer.getMessage()).get("response").asText()); + assertThat(responseId).isEqualTo(requestId); + assertThat(mapper.readTree(responseFromServer.getMessage()).get("response").asText()).isEqualTo("requestReceived"); // Make the default rule chain a root again - ResponseEntity rootRuleChainResponse = restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/ruleChain/{ruleChainId}/root", - null, - RuleChain.class, - defaultRuleChainId); - Assert.assertTrue(rootRuleChainResponse.getStatusCode().is2xxSuccessful()); + testRestClient.setRootRuleChain(defaultRuleChainId); // Delete the created rule chain - restClient.getRestTemplate().delete(HTTPS_URL + "/api/ruleChain/{ruleChainId}", ruleChainId); - restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId()); + testRestClient.deleteRuleChain(ruleChainId); } @Test public void deviceDeletedClosingSession() throws Exception { - restClient.login("tenant@thingsboard.org", "tenant"); - String deviceForDeletingTestName = "Device for deleting notification test"; - Device device = createDevice(deviceForDeletingTestName); - DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); + DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); MqttMessageListener listener = new MqttMessageListener(); MqttClient mqttClient = getMqttClient(deviceCredentials, listener); - restClient.deleteDevice(device.getId()); + testRestClient.deleteDeviceIfExists(device.getId()); TimeUnit.SECONDS.sleep(3 * timeoutMultiplier); - Assert.assertFalse(mqttClient.isConnected()); + assertThat(mqttClient.isConnected()).isFalse(); } private RuleChainId createRootRuleChainForRpcResponse() throws Exception { RuleChain newRuleChain = new RuleChain(); newRuleChain.setName("testRuleChain"); - ResponseEntity ruleChainResponse = restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/ruleChain", - newRuleChain, - RuleChain.class); - Assert.assertTrue(ruleChainResponse.getStatusCode().is2xxSuccessful()); - RuleChain ruleChain = ruleChainResponse.getBody(); + + RuleChain ruleChain = testRestClient.postRootRuleChain(newRuleChain); JsonNode configuration = mapper.readTree(this.getClass().getClassLoader().getResourceAsStream("RpcResponseRuleChainMetadata.json")); RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); @@ -376,37 +336,22 @@ public class MqttClientTest extends AbstractContainerTest { ruleChainMetaData.setNodes(Arrays.asList(mapper.treeToValue(configuration.get("nodes"), RuleNode[].class))); ruleChainMetaData.setConnections(Arrays.asList(mapper.treeToValue(configuration.get("connections"), NodeConnectionInfo[].class))); - ResponseEntity ruleChainMetadataResponse = restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/ruleChain/metadata", - ruleChainMetaData, - RuleChainMetaData.class); - Assert.assertTrue(ruleChainMetadataResponse.getStatusCode().is2xxSuccessful()); + testRestClient.postRuleChainMetadata(ruleChainMetaData); // Set a new rule chain as root - ResponseEntity rootRuleChainResponse = restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/ruleChain/{ruleChainId}/root", - null, - RuleChain.class, - ruleChain.getId()); - Assert.assertTrue(rootRuleChainResponse.getStatusCode().is2xxSuccessful()); - + testRestClient.setRootRuleChain(ruleChain.getId()); return ruleChain.getId(); } private RuleChainId getDefaultRuleChainId() { - ResponseEntity> ruleChains = restClient.getRestTemplate().exchange( - HTTPS_URL + "/api/ruleChains?pageSize=40&page=0&textSearch=", - HttpMethod.GET, - null, - new ParameterizedTypeReference>() { - }); - - Optional defaultRuleChain = ruleChains.getBody().getData() + PageData ruleChains = testRestClient.getRuleChains(new PageLink(40, 0)); + + Optional defaultRuleChain = ruleChains.getData() .stream() .filter(RuleChain::isRoot) .findFirst(); if (!defaultRuleChain.isPresent()) { - Assert.fail("Root rule chain wasn't found"); + fail("Root rule chain wasn't found"); } return defaultRuleChain.get().getId(); } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java index 25a8946431..f844be8a27 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java @@ -16,10 +16,6 @@ package org.thingsboard.server.msa.connectivity; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.Sets; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -28,15 +24,15 @@ import io.netty.buffer.Unpooled; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.springframework.http.ResponseEntity; -import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.springframework.http.HttpStatus; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttHandler; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; @@ -48,40 +44,40 @@ import org.thingsboard.server.msa.AbstractContainerTest; import org.thingsboard.server.msa.WsClient; import org.thingsboard.server.msa.mapper.WsTelemetryResponse; -import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Random; +import java.util.*; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.DataConstants.DEVICE; +import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; +import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultGatewayPrototype; + @Slf4j public class MqttGatewayClientTest extends AbstractContainerTest { - Device gatewayDevice; - MqttClient mqttClient; - Device createdDevice; - MqttMessageListener listener; + private Device gatewayDevice; + private MqttClient mqttClient; + private Device createdDevice; + private MqttMessageListener listener; - @Before + @BeforeMethod public void createGateway() throws Exception { - restClient.login("tenant@thingsboard.org", "tenant"); - this.gatewayDevice = createGatewayDevice(); - Optional gatewayDeviceCredentials = restClient.getDeviceCredentialsByDeviceId(gatewayDevice.getId()); - Assert.assertTrue(gatewayDeviceCredentials.isPresent()); + testRestClient.login("tenant@thingsboard.org", "tenant"); + gatewayDevice = testRestClient.postDevice("", defaultGatewayPrototype()); + DeviceCredentials gatewayDeviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(gatewayDevice.getId()); + this.listener = new MqttMessageListener(); - this.mqttClient = getMqttClient(gatewayDeviceCredentials.get(), listener); + this.mqttClient = getMqttClient(gatewayDeviceCredentials, listener); this.createdDevice = createDeviceThroughGateway(mqttClient, gatewayDevice); } - @After - public void removeGateway() throws Exception { - restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + this.gatewayDevice.getId()); - restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + this.createdDevice.getId()); + @AfterMethod + public void removeGateway() { + testRestClient.deleteDeviceIfExists(this.gatewayDevice.getId()); + testRestClient.deleteDeviceIfExists(this.createdDevice.getId()); this.listener = null; this.mqttClient = null; this.createdDevice = null; @@ -95,40 +91,38 @@ public class MqttGatewayClientTest extends AbstractContainerTest { log.info("Received telemetry: {}", actualLatestTelemetry); wsClient.closeBlocking(); - Assert.assertEquals(4, actualLatestTelemetry.getData().size()); - Assert.assertEquals(Sets.newHashSet("booleanKey", "stringKey", "doubleKey", "longKey"), - actualLatestTelemetry.getLatestValues().keySet()); + assertThat(actualLatestTelemetry.getData()).hasSize(4); + assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("booleanKey", "stringKey", "doubleKey", "longKey")); - Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", Boolean.TRUE.toString())); - Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", "value1")); - Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0))); - Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73))); + assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString()); + assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1"); + assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0)); + assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73)); } @Test public void telemetryUploadWithTs() throws Exception { long ts = 1451649600512L; - restClient.login("tenant@thingsboard.org", "tenant"); WsClient wsClient = subscribeToWebSocket(createdDevice.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS); mqttClient.publish("v1/gateway/telemetry", Unpooled.wrappedBuffer(createGatewayPayload(createdDevice.getName(), ts).toString().getBytes())).get(); WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage(); log.info("Received telemetry: {}", actualLatestTelemetry); wsClient.closeBlocking(); - Assert.assertEquals(4, actualLatestTelemetry.getData().size()); - Assert.assertEquals(getExpectedLatestValues(ts), actualLatestTelemetry.getLatestValues()); + assertThat(actualLatestTelemetry.getData()).hasSize(4); + assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("booleanKey", "stringKey", "doubleKey", "longKey")); - Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", ts, Boolean.TRUE.toString())); - Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", ts, "value1")); - Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", ts, Double.toString(42.0))); - Assert.assertTrue(verify(actualLatestTelemetry, "longKey", ts, Long.toString(73))); + assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString()); + assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1"); + assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0)); + assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73)); } @Test public void publishAttributeUpdateToServer() throws Exception { - Optional createdDeviceCredentials = restClient.getDeviceCredentialsByDeviceId(createdDevice.getId()); - Assert.assertTrue(createdDeviceCredentials.isPresent()); + testRestClient.getDeviceCredentialsByDeviceId(createdDevice.getId()); + WsClient wsClient = subscribeToWebSocket(createdDevice.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS); JsonObject clientAttributes = new JsonObject(); clientAttributes.addProperty("attr1", "value1"); @@ -142,20 +136,18 @@ public class MqttGatewayClientTest extends AbstractContainerTest { log.info("Received attributes: {}", actualLatestTelemetry); wsClient.closeBlocking(); - Assert.assertEquals(4, actualLatestTelemetry.getData().size()); - Assert.assertEquals(Sets.newHashSet("attr1", "attr2", "attr3", "attr4"), - actualLatestTelemetry.getLatestValues().keySet()); + assertThat(actualLatestTelemetry.getData()).hasSize(4); + assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("attr1", "attr2", "attr3", "attr4")); - Assert.assertTrue(verify(actualLatestTelemetry, "attr1", "value1")); - Assert.assertTrue(verify(actualLatestTelemetry, "attr2", Boolean.TRUE.toString())); - Assert.assertTrue(verify(actualLatestTelemetry, "attr3", Double.toString(42.0))); - Assert.assertTrue(verify(actualLatestTelemetry, "attr4", Long.toString(73))); + assertThat(actualLatestTelemetry.getDataValuesByKey("attr1").get(1)).isEqualTo("value1"); + assertThat(actualLatestTelemetry.getDataValuesByKey("attr2").get(1)).isEqualTo(Boolean.TRUE.toString()); + assertThat(actualLatestTelemetry.getDataValuesByKey("attr3").get(1)).isEqualTo(Double.toString(42.0)); + assertThat(actualLatestTelemetry.getDataValuesByKey("attr4").get(1)).isEqualTo(Long.toString(73)); } @Test public void responseDataOnAttributesRequestCheck() throws Exception { - Optional createdDeviceCredentials = restClient.getDeviceCredentialsByDeviceId(createdDevice.getId()); - Assert.assertTrue(createdDeviceCredentials.isPresent()); + testRestClient.getDeviceCredentialsByDeviceId(createdDevice.getId()); JsonObject sharedAttributes = new JsonObject(); sharedAttributes.addProperty("attr1", "value1"); sharedAttributes.addProperty("attr2", true); @@ -163,11 +155,8 @@ public class MqttGatewayClientTest extends AbstractContainerTest { sharedAttributes.addProperty("attr4", 73); mqttClient.on("v1/gateway/attributes/response", listener, MqttQoS.AT_LEAST_ONCE).get(); - ResponseEntity sharedAttributesResponse = restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE", - mapper.readTree(sharedAttributes.toString()), ResponseEntity.class, - createdDevice.getId()); - Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful()); + + testRestClient.postTelemetryAttribute(DataConstants.DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString())); var event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); JsonObject requestData = new JsonObject(); @@ -181,8 +170,8 @@ public class MqttGatewayClientTest extends AbstractContainerTest { event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); JsonObject responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject(); - Assert.assertTrue(responseData.has("value")); - Assert.assertEquals(sharedAttributes.get("attr1").getAsString(), responseData.get("value").getAsString()); + assertThat(responseData.has("value")).isTrue(); + assertThat(responseData.get("value").getAsString()).isEqualTo(sharedAttributes.get("attr1").getAsString()); requestData = new JsonObject(); requestData.addProperty("id", 1); @@ -198,9 +187,9 @@ public class MqttGatewayClientTest extends AbstractContainerTest { event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject(); - Assert.assertTrue(responseData.has("values")); - Assert.assertEquals(sharedAttributes.get("attr1").getAsString(), responseData.get("values").getAsJsonObject().get("attr1").getAsString()); - Assert.assertEquals(sharedAttributes.get("attr2").getAsString(), responseData.get("values").getAsJsonObject().get("attr2").getAsString()); + assertThat(responseData.has("value")).isTrue(); + assertThat(responseData.get("values").getAsJsonObject().get("attr1").getAsString()).isEqualTo(sharedAttributes.get("attr1").getAsString()); + assertThat(responseData.get("values").getAsJsonObject().get("attr2").getAsString()).isEqualTo(sharedAttributes.get("attr2").getAsString()); requestData = new JsonObject(); requestData.addProperty("id", 1); @@ -216,9 +205,9 @@ public class MqttGatewayClientTest extends AbstractContainerTest { event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject(); - Assert.assertTrue(responseData.has("values")); - Assert.assertEquals(sharedAttributes.get("attr1").getAsString(), responseData.get("values").getAsJsonObject().get("attr1").getAsString()); - Assert.assertEquals(1, responseData.get("values").getAsJsonObject().entrySet().size()); + assertThat(responseData.has("values")).isTrue(); + assertThat(responseData.get("values").getAsJsonObject().get("attr1").getAsString()).isEqualTo(sharedAttributes.get("attr1").getAsString()); + assertThat(responseData.get("values").getAsJsonObject().entrySet()).hasSize(1); } @Test @@ -237,11 +226,9 @@ public class MqttGatewayClientTest extends AbstractContainerTest { log.info("Received ws telemetry: {}", actualLatestTelemetry); wsClient.closeBlocking(); - Assert.assertEquals(1, actualLatestTelemetry.getData().size()); - Assert.assertEquals(Sets.newHashSet("clientAttr"), - actualLatestTelemetry.getLatestValues().keySet()); - - Assert.assertTrue(verify(actualLatestTelemetry, "clientAttr", clientAttributeValue)); + assertThat(actualLatestTelemetry.getData()).hasSize(1); + assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnly("clientAttr"); + assertThat(actualLatestTelemetry.getDataValuesByKey("clientAttr").get(1)).isEqualTo(clientAttributeValue); // Add a new shared attribute JsonObject sharedAttributes = new JsonObject(); @@ -251,16 +238,12 @@ public class MqttGatewayClientTest extends AbstractContainerTest { // Subscribe for attribute update event mqttClient.on("v1/gateway/attributes", listener, MqttQoS.AT_LEAST_ONCE).get(); - ResponseEntity sharedAttributesResponse = restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE", - mapper.readTree(sharedAttributes.toString()), ResponseEntity.class, - createdDevice.getId()); - Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful()); + testRestClient.postTelemetryAttribute(DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString())); MqttEvent sharedAttributeEvent = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); // Catch attribute update event - Assert.assertNotNull(sharedAttributeEvent); - Assert.assertEquals("v1/gateway/attributes", sharedAttributeEvent.getTopic()); + assertThat(sharedAttributeEvent).isNotNull(); + assertThat(sharedAttributeEvent.getTopic()).isEqualTo("v1/gateway/attributes"); // Subscribe to attributes response mqttClient.on("v1/gateway/attributes/response", listener, MqttQoS.AT_LEAST_ONCE).get(); @@ -288,15 +271,11 @@ public class MqttGatewayClientTest extends AbstractContainerTest { gatewaySharedAttributeValue.addProperty("device", createdDevice.getName()); gatewaySharedAttributeValue.add("data", sharedAttributes); - ResponseEntity sharedAttributesResponse = restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE", - mapper.readTree(sharedAttributes.toString()), ResponseEntity.class, - createdDevice.getId()); - Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful()); + testRestClient.postTelemetryAttribute(DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString())); MqttEvent event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); - Assert.assertEquals(sharedAttributeValue, - mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText()); + assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText()) + .isEqualTo(sharedAttributeValue); // Update the shared attribute value JsonObject updatedSharedAttributes = new JsonObject(); @@ -307,15 +286,10 @@ public class MqttGatewayClientTest extends AbstractContainerTest { gatewayUpdatedSharedAttributeValue.addProperty("device", createdDevice.getName()); gatewayUpdatedSharedAttributeValue.add("data", updatedSharedAttributes); - ResponseEntity updatedSharedAttributesResponse = restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE", - mapper.readTree(updatedSharedAttributes.toString()), ResponseEntity.class, - createdDevice.getId()); - Assert.assertTrue(updatedSharedAttributesResponse.getStatusCode().is2xxSuccessful()); - + testRestClient.postTelemetryAttribute(DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(updatedSharedAttributes.toString())); event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); - Assert.assertEquals(updatedSharedAttributeValue, - mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText()); + assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText()) + .isEqualTo(updatedSharedAttributeValue); } @Test @@ -330,35 +304,18 @@ public class MqttGatewayClientTest extends AbstractContainerTest { JsonObject serverRpcPayload = new JsonObject(); serverRpcPayload.addProperty("method", "getValue"); serverRpcPayload.addProperty("params", true); - ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(getClass().getSimpleName()))); - ListenableFuture future = service.submit(() -> { - try { - return restClient.getRestTemplate() - .postForEntity(HTTPS_URL + "/api/rpc/twoway/{deviceId}", - mapper.readTree(serverRpcPayload.toString()), String.class, - createdDevice.getId()); - } catch (IOException e) { - return ResponseEntity.badRequest().build(); - } - }); + + JsonNode response = testRestClient.postServerSideRpc(createdDevice.getId(), mapper.readTree(serverRpcPayload.toString())); // Wait for RPC call from the server and send the response MqttEvent requestFromServer = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); - service.shutdownNow(); - - Assert.assertNotNull(requestFromServer); - Assert.assertNotNull(requestFromServer.getMessage()); - - JsonObject requestFromServerJson = new JsonParser().parse(requestFromServer.getMessage()).getAsJsonObject(); - - Assert.assertEquals(createdDevice.getName(), requestFromServerJson.get("device").getAsString()); - - JsonObject requestFromServerData = requestFromServerJson.get("data").getAsJsonObject(); - - Assert.assertEquals("getValue", requestFromServerData.get("method").getAsString()); - Assert.assertTrue(requestFromServerData.get("params").getAsBoolean()); - - int requestId = requestFromServerData.get("id").getAsInt(); + assertThat(requestFromServer).isNotNull(); + assertThat(requestFromServer.getMessage()).isNotNull(); + JsonNode requestFromServerJson = JacksonUtil.toJsonNode(requestFromServer.getMessage()); + assertThat(requestFromServerJson.get("device").asText()).isEqualTo(createdDevice.getName()); + assertThat(requestFromServerJson.get("data").get("method").asText()).isEqualTo("getValue"); + assertThat(requestFromServerJson.get("data").get("params").asText()).isEqualTo("true"); + int requestId = requestFromServerJson.get("data").get("id").asInt(); JsonObject clientResponse = new JsonObject(); clientResponse.addProperty("response", "someResponse"); @@ -369,16 +326,14 @@ public class MqttGatewayClientTest extends AbstractContainerTest { // Send a response to the server's RPC request mqttClient.publish(gatewayRpcTopic, Unpooled.wrappedBuffer(gatewayResponse.toString().getBytes())).get(); - ResponseEntity serverResponse = future.get(5 * timeoutMultiplier, TimeUnit.SECONDS); - Assert.assertTrue(serverResponse.getStatusCode().is2xxSuccessful()); - Assert.assertEquals(clientResponse.toString(), serverResponse.getBody()); + + assertThat(response).isEqualTo(clientResponse.getAsJsonObject()); } @Test public void deviceCreationAfterDeleted() throws Exception { - restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + this.createdDevice.getId()); - Optional deletedDevice = restClient.getDeviceById(this.createdDevice.getId()); - Assert.assertTrue(deletedDevice.isEmpty()); + testRestClient.deleteDevice(this.createdDevice.getId()); + testRestClient.getDeviceById(this.createdDevice.getId(), HttpStatus.NOT_FOUND.value()); this.createdDevice = createDeviceThroughGateway(mqttClient, gatewayDevice); } @@ -397,13 +352,13 @@ public class MqttGatewayClientTest extends AbstractContainerTest { log.info(gatewayAttributesRequest.toString()); mqttClient.publish("v1/gateway/attributes/request", Unpooled.wrappedBuffer(gatewayAttributesRequest.toString().getBytes())).get(); MqttEvent clientAttributeEvent = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); - Assert.assertNotNull(clientAttributeEvent); + assertThat(clientAttributeEvent).isNotNull(); JsonObject responseMessage = new JsonParser().parse(Objects.requireNonNull(clientAttributeEvent).getMessage()).getAsJsonObject(); - Assert.assertEquals(messageId, responseMessage.get("id").getAsInt()); - Assert.assertEquals(createdDevice.getName(), responseMessage.get("device").getAsString()); - Assert.assertEquals(3, responseMessage.entrySet().size()); - Assert.assertEquals(expectedValue, responseMessage.get("value").getAsString()); + assertThat(responseMessage.get("id").getAsInt()).isEqualTo(messageId); + assertThat(responseMessage.get("device").getAsString()).isEqualTo(createdDevice.getName()); + assertThat(responseMessage.entrySet()).hasSize(3); + assertThat(responseMessage.get("value").getAsString()).isEqualTo(expectedValue); } private Device createDeviceThroughGateway(MqttClient mqttClient, Device gatewayDevice) throws Exception { @@ -418,17 +373,12 @@ public class MqttGatewayClientTest extends AbstractContainerTest { TimeUnit.SECONDS.sleep(30); } - List relations = restClient.findByFrom(gatewayDevice.getId(), RelationTypeGroup.COMMON); - - Assert.assertEquals(1, relations.size()); + List relations = testRestClient.findRelationByFrom(gatewayDevice.getId(), RelationTypeGroup.COMMON); + assertThat(relations).hasSize(1); EntityId createdEntityId = relations.get(0).getTo(); DeviceId createdDeviceId = new DeviceId(createdEntityId.getId()); - Optional createdDevice = restClient.getDeviceById(createdDeviceId); - - Assert.assertTrue(createdDevice.isPresent()); - - return createdDevice.get(); + return testRestClient.getDeviceById(createdDeviceId); } private MqttClient getMqttClient(DeviceCredentials deviceCredentials, MqttMessageListener listener) throws InterruptedException, ExecutionException { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java new file mode 100644 index 0000000000..d7edcb6699 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2022 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.msa.prototypes; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; + +public class DevicePrototypes { + public static Device defaultDevicePrototype(String name){ + Device device = new Device(); + device.setName(name + RandomStringUtils.randomAlphanumeric(7)); + device.setType("DEFAULT"); + return device; + } + + public static Device defaultGatewayPrototype() throws JsonProcessingException { + String isGateway = "{\"gateway\":true}"; + JsonNode additionalInfo = JacksonUtil.valueToTree(isGateway); + Device gatewayDeviceTemplate = new Device(); + gatewayDeviceTemplate.setName("mqtt_gateway " + RandomStringUtils.randomAlphabetic(5)); + gatewayDeviceTemplate.setType("gateway"); + gatewayDeviceTemplate.setAdditionalInfo(additionalInfo); + return gatewayDeviceTemplate; + } +} diff --git a/msa/black-box-tests/src/test/resources/config.properties b/msa/black-box-tests/src/test/resources/config.properties new file mode 100644 index 0000000000..419c73185d --- /dev/null +++ b/msa/black-box-tests/src/test/resources/config.properties @@ -0,0 +1,2 @@ +tb.baseUrl=http://localhost:8080 +tb.wsUrl=ws://localhost:8080 From 92dd5ff09f6c283842c2b47b31fb440c129f28c8 Mon Sep 17 00:00:00 2001 From: wanglei100 Date: Sun, 6 Nov 2022 22:36:49 +0800 Subject: [PATCH 039/226] =?UTF-8?q?=E4=B8=8Elocale.constant-en=5FUS.json?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=AF=B9=E5=BA=94=E7=9A=84=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E6=B1=89=E5=8C=96=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assets/locale/locale.constant-zh_CN.json | 8062 ++++++++++------- 1 file changed, 4783 insertions(+), 3279 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index 8263cc1614..65eb61136f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -1,3282 +1,4786 @@ { - "access": { - "access-forbidden": "禁止访问", - "access-forbidden-text": "您没有访问此位置的权限
如果您仍希望访问此位置,请尝试使用其他用户登录。", - "permission-denied": "权限被拒绝", - "permission-denied-text": "您没有执行此操作的权限!", - "refresh-token-expired": "会话已过期", - "refresh-token-failed": "无法刷新会话", - "unauthorized": "未授权", - "unauthorized-access": "未授权访问", - "unauthorized-access-text": "您需要登陆才能访问这个资源!" - }, - "action": { - "activate": "激活", - "add": "添加", - "apply": "应用", - "apply-changes": "应用更改", - "assign": "分配", - "back": "后退", - "cancel": "取消", - "clear-search": "清除查询", - "close": "关闭", - "continue": "继续", - "copy": "复制", - "copy-reference": "复制引用", - "create": "创建", - "decline-changes": "撤销更改", - "delete": "删除", - "discard-changes": "放弃更改", - "download": "下载", - "drag": "拖拽", - "edit": "编辑", - "edit-mode": "编辑模式", - "enter-edit-mode": "进入编辑模式", - "export": "导出", - "hide": "隐藏", - "import": "导入", - "make-private": "私有", - "next-with-label": "下一个:{{label}}", - "no": "否", - "ok": "确定", - "paste": "粘贴", - "paste-reference": "粘贴引用", - "read-more": "阅读更多", - "refresh": "刷新", - "remove": "移除", - "run": "运行", - "save": "保存", - "saveAs": "另存为", - "search": "查询", - "select": "选择", - "share": "分享", - "share-via": "通过 {{provider}}分享", - "sign-in": "登录!", - "suspend": "暂停", - "unassign": "取消分配", - "undo": "撤销", - "update": "更新", - "view": "查看", - "yes": "是" - }, - "admin": { - "aws-access-key-id": "AWS访问密钥ID", - "aws-access-key-id-required": "需要访问AWS密钥ID", - "aws-region": "AWS地区", - "aws-region-required": "AWS 区域必填", - "aws-secret-access-key": "AWS秘密访问密钥", - "aws-secret-access-key-required": "AWS 访问密钥必填", - "base-url": "基本URL", - "base-url-required": "基本URL必填。", - "domain-name": "域名", - "domain-name-unique": "域名和协议必须是唯一的。", - "enable-proxy": "启用代理", - "enable-tls": "启用TLS", - "error-verification-url": "域名不应包含符号 “/” 和 “:”。例:thingsboard.io", - "general": "基本设置", - "general-policy": "基本策略", - "general-settings": "基本设置", - "home-settings": "首页设置", - "mail-from": "邮件来自", - "mail-from-required": "邮件发件人必填。", - "max-failed-login-attempts": "登录失败之前的最大登录尝试次数", - "minimum-digits": "最少数字位数", - "minimum-digits-range": "最少数字位数不能为负数", - "minimum-lowercase-letters": "最少小写字母位数", - "minimum-lowercase-letters-range": "最少小写字母位数不能为负数", - "minimum-max-failed-login-attempts-range": "登录失败次数不能为负数", - "minimum-password-length": "最小密码长度", - "minimum-password-length-range": "最小密码长度应在5到50之间", - "minimum-password-length-required": "最小密码长度必填", - "minimum-special-characters": "最少特殊字符位数", - "minimum-special-characters-range": "最少特殊字符位数不能为负数", - "minimum-uppercase-letters": "最少大写字母位数", - "minimum-uppercase-letters-range": "最少大写字母位数不能为负数", - "number-from": "发送方电话号码", - "number-from-required": "发送方电话号码必填。", - "number-to": "电话号码至", - "number-to-required": "电话号码必填。", - "oauth2": { - "access-token-uri": "访问令牌URI", - "access-token-uri-required": "访问令牌 URI 必填。", - "activate-user": "激活用户", - "add-domain": "添加域", - "add-provider": "添加 Provider", - "allow-user-creation": "允许用户创建", - "always-fullscreen": "始终全屏", - "authorization-uri": "授权URI", - "authorization-uri-required": "授权 URI 必填。", - "client-authentication-method": "客户端身份验证方法", - "client-id": "客户端ID", - "client-id-required": "客户端 ID 必填。", - "client-secret": "客户机密", - "client-secret-required": "需要客户端密码。", - "copy-redirect-uri": "复制重定向URI", - "custom-setting": "自定义设置", - "customer-name-pattern": "客户名称模式", - "default-dashboard-name": "默认仪表板名称", - "delete-domain": "删除域", - "delete-domain-text": "请注意:确认后,域和所有 provider data 将不可恢复。", - "delete-domain-title": "确定要删除域 '{{domainName}}' 的设置吗?", - "delete-provider": "删除 Provider", - "delete-registration-text": "请注意:确认后 provider data 将不可恢复。", - "delete-registration-title": "确定要删除 provider '{{name}}' 吗?", - "domain-schema-http": "HTTP", - "domain-schema-https": "HTTPS", - "domain-schema-mixed": "HTTP+HTTPS", - "email-attribute-key": "电子邮件属性键", - "email-attribute-key-required": "电子邮件属性密钥必填。", - "enable": "启用OAuth2设置", - "first-name-attribute-key": "名字属性键", - "general": "General", - "jwk-set-uri": "JSON Web Key URI", - "last-name-attribute-key": "姓氏属性键", - "login-button-icon": "登录按钮图标", - "login-button-label": "Provider 标签", - "login-button-label-placeholder": "使用 $(Provider label) 登录", - "login-button-label-required": "标签必填。", - "login-provider": "Login provider", - "mapper": "Mapper", - "new-domain": "新建域", - "oauth2": "OAuth2", - "protocol": "协议", - "redirect-uri-template": "重定向URI模板", - "registration-id": "注册ID", - "registration-id-required": "注册 ID 必填。", - "registration-id-unique": "系统的注册ID必须是唯一的。", - "scope": "范围", - "scope-required": "范围必填。", - "tenant-name-pattern": "租户名称模式", - "tenant-name-pattern-required": "租户名称模式必填。", - "tenant-name-strategy": "租户名称策略", - "type": "映射器类型", - "uri-pattern-error": "无效的URI格式。", - "url": "统一资源定位地址", - "url-pattern": "无效的URL格式。", - "url-required": "URL 必填。", - "user-info-uri": "用户信息URI", - "user-info-uri-required": "用户信息 URI 必填。", - "user-name-attribute-name": "用户名属性键", - "user-name-attribute-name-required": "用户名属性密钥必填" - }, - "outgoing-mail": "发送邮件", - "outgoing-mail-settings": "发送邮件设置", - "password-expiration-period-days": "密码有效期(天)", - "password-expiration-period-days-range": "密码过期期限(天)不能为负", - "password-policy": "密码策略", - "password-reuse-frequency-days": "密码重用频率(天)", - "password-reuse-frequency-days-range": "天内密码重用频率不能为负", - "phone-number-hint": "E.164格式的手机号码,例如+19995550123", - "phone-number-pattern": "手机号码无效。应为E.164格式,例如+19995550123。", - "prohibit-different-url": "禁止从客户端请求头中使用主机名", - "prohibit-different-url-hint": "应为生产环境启用此设置。禁用时可能会导致安全问题", - "proxy-host": "代理主机", - "proxy-host-required": "代理主机必填。", - "proxy-password": "代理密码", - "proxy-port": "代理端口", - "proxy-port-range": "代理端口应在1到65535之间。", - "proxy-port-required": "代理端口必填。", - "proxy-user": "代理用户", - "security-settings": "安全设置", - "send-test-mail": "发送测试邮件", - "send-test-sms": "发送测试短信", - "sms-message": "短信", - "sms-message-max-length": "短信长度不能超过1600个字符", - "sms-message-required": "短消息内容必填。", - "sms-provider": "SMS 服务商", - "sms-provider-settings": "SMS 服务商设置", - "sms-provider-type": "SMS 服务商类型", - "sms-provider-type-aws-sns": "亚马逊社交网站", - "sms-provider-type-required": "SMS 服务商类型必填。", - "sms-provider-type-twilio": "Twilio", - "smtp-host": "SMTP主机", - "smtp-host-required": "SMTP主机必填。", - "smtp-port": "SMTP端口", - "smtp-port-invalid": "这看起来不是有效的smtp端口。", - "smtp-port-required": "您必须提供一个smtp端口。", - "smtp-protocol": "SMTP协议", - "system-settings": "系统设置", - "test-mail-sent": "测试邮件发送成功!", - "test-sms-sent": "测试短信发送成功!", - "timeout-invalid": "这看起来不像有效的超时值。", - "timeout-msec": "超时时间(毫秒)", - "timeout-required": "超时必填。", - "tls-version": "TLS版本", - "twilio-account-sid": "Twilio帐户SID", - "twilio-account-sid-required": "Twilio 帐户的 SID 必填", - "twilio-account-token": "Twilio帐户令牌", - "twilio-account-token-required": "Twilio 帐户令牌必填", - "user-lockout-notification-email": "如果用户帐户锁定,请发送通知到电子邮件", - "queue-select-name": "选择队列名称", - "queue-name": "名称", - "queue-name-required": "队列名称必填。", - "queues": "队列", - "queue-partitions": "分区", - "queue-submit-strategy": "提交策略", - "queue-processing-strategy": "处理策略", - "queue-configuration": "队列配置", - "repository-settings": "仓库设置", - "repository-url": "仓库 URL", - "repository-url-required": "仓库 URL 必填。", - "default-branch": "默认分支名称", - "authentication-settings": "身份验证设置", - "auth-method": "身份验证方法", - "auth-method-username-password": "密码/访问令牌", - "auth-method-private-key": "私钥", - "password-access-token": "密码/访问令牌", - "change-password-access-token": "更改密码/访问令牌", - "private-key": "私钥", - "drop-private-key-file-or": "拖放私钥文件或", - "passphrase": "口令", - "enter-passphrase": "输入口令", - "change-passphrase": "更改口令", - "check-access": "检查访问权限", - "check-repository-access-success": "已成功验证仓库访问!", - "delete-repository-settings-title": "确定要删除仓库设置吗?", - "delete-repository-settings-text": "请注意:确认后,仓库设置将被删除,版本控制功能将不可用。", - "auto-commit-settings": "自动提交设置", - "auto-commit-entities": "自动提交实体", - "no-auto-commit-entities-prompt": "没有设置自动提交的实体", - "delete-auto-commit-settings-title": "确定要删除自动提交设置吗?", - "delete-auto-commit-settings-text": "请注意:确认后,自动提交设置将被删除,所有实体的自动提交将被禁用。", - "2fa": { - "2fa": "双因素身份验证", - "available-providers": "可用选项", - "issuer-name": "发行者名称", - "issuer-name-required": "发行者名称必填。", - "max-verification-failures-before-user-lockout": "用户锁定前最大验证失败次数", - "max-verification-failures-before-user-lockout-pattern": "最大验证失败次数必须为正整数。", - "number-of-checking-attempts": "检查尝试次数", - "number-of-checking-attempts-pattern": "检查尝试次数必须为正整数。", - "number-of-checking-attempts-required": "检查尝试次数必填。", - "number-of-codes": "验证码数量", - "number-of-codes-pattern": "验证码数量必须为正整数。", - "number-of-codes-required": "验证码数量必填。", - "provider": "Provider", - "retry-verification-code-period": "重试验证码周期(秒)", - "retry-verification-code-period-pattern": "最短时间为5秒", - "retry-verification-code-period-required": "重试验证代码周期必填。", - "total-allowed-time-for-verification": "总允许验证时间(秒)", - "total-allowed-time-for-verification-pattern": "总允许验证最小时间为60秒", - "total-allowed-time-for-verification-required": "总允许验证时间必填。", - "use-system-two-factor-auth-settings": "使用系统双因素身份验证设置", - "verification-code-check-rate-limit": "验证码检查速率限制", - "verification-code-lifetime": "验证码生存期(秒)", - "verification-code-lifetime-pattern": "验证码生存期必须为正整数。", - "verification-code-lifetime-required": "验证码生存期必填。", - "verification-message-template": "验证消息模板", - "verification-limitations": "验证限制", - "verification-message-template-pattern": "验证消息需要包含模板:${code}", - "verification-message-template-required": "验证消息模板必填。", - "within-time": "在时间内 (秒)", - "within-time-pattern": "时间必须是正整数。", - "within-time-required": "时间必填。" - } - }, - "aggregation": { - "aggregation": "聚合", - "avg": "平均值", - "count": "计数", - "function": "数据聚合功能", - "group-interval": "分组间隔", - "limit": "限制数", - "max": "最大值", - "min": "最小值", - "none": "无", - "sum": "求和" - }, - "alarm": { - "ack-time": "确认时间", - "acknowledge": "应答", - "aknowledge-alarm-text": "确定要确认告警吗?", - "aknowledge-alarm-title": "确认告警", - "aknowledge-alarms-text": "确定要确认 { count, plural, 1 {# 个告警} other {# 个告警} }吗?", - "aknowledge-alarms-title": "确认 { count, plural, 1 {# 个告警} other {# 个告警} }", - "alarm": "告警", - "alarm-details": "告警详细信息", - "alarm-filter": "告警筛选器", - "alarm-required": "告警必填", - "alarm-severity-list": "警报严重性列表", - "alarm-status": "告警状态", - "alarm-status-filter": "告警状态筛选器", - "alarm-status-list": "告警状态列表", - "alarm-type-list": "告警类型列表", - "alarms": "告警", - "any-severity": "任何严重程度", - "any-status": "任何状态", - "any-type": "任何类型", - "clear": "清除", - "clear-alarm-text": "确定要清除警报吗?", - "clear-alarm-title": "清除警报", - "clear-alarms-text": "确定要清除 { count, plural, 1 {# 个告警} other {# 个告警} }?", - "clear-alarms-title": "清除 { count, plural, 1 {# 个告警} other {# 个告警} }", - "clear-time": "清除时间", - "created-time": "创建时间", - "details": "详情", - "display-status": { - "ACTIVE_ACK": "激活已确认", - "ACTIVE_UNACK": "激活未确认", - "CLEARED_ACK": "清除已确认", - "CLEARED_UNACK": "清除未确认" - }, - "end-time": "结束时间", - "fetch-size": "获取大小", - "fetch-size-error-min": "最小值为10。", - "fetch-size-required": "Fetch size 必填。", - "max-count-load": "要加载的最大告警数(0-无限制)", - "max-count-load-error-min": "最小值为0。", - "max-count-load-required": "加载的最大告警数必填。", - "min-polling-interval-message": "轮询间隔至少是1秒。", - "no-alarms-matching": "未找到匹配 '{{entity}}' 的告警", - "no-alarms-prompt": "未发现告警", - "no-data": "无数据显示", - "originator": "发起者", - "originator-type": "发起者类型", - "polling-interval": "告警轮询间隔(秒)", - "polling-interval-required": "告警轮询间隔必填。", - "search": "查找告警", - "search-propagated-alarms": "检索已传递的警报", - "search-status": { - "ACK": "已确认", - "ACTIVE": "激活", - "ANY": "所有", - "CLEARED": "已清除", - "UNACK": "未确认" - }, - "select-alarm": "选择告警", - "selected-alarms": "已选择 { count, plural, 1 {# 个告警} other {# 个告警} }", - "severity": "严重程度", - "severity-critical": "危险", - "severity-indeterminate": "不确定", - "severity-major": "重要", - "severity-minor": "次要", - "severity-warning": "警告", - "start-time": "开始时间", - "status": "状态", - "type": "类型" - }, - "alias": { - "add": "添加别名", - "all-entities": "所有实体", - "any-relation": "不限", - "default-entity-parameter-name": "默认", - "default-state-entity": "默认状态实体", - "duplicate-alias": "别名已经存在。", - "edit": "编辑别名", - "entity-filter": "实体筛选器", - "entity-filter-no-entity-matched": "未找到匹配指定筛选条件的实体。", - "filter-type": "筛选器类型", - "filter-type-apiUsageState": "Api使用状态", - "filter-type-asset-search-query": "资产搜索查询", - "filter-type-asset-search-query-description": "类型为 {{assetTypes}} 且具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的资产", - "filter-type-asset-type": "资产类型", - "filter-type-asset-type-and-name-description": "类型为 '{{assetType}}' 且以 '{{prefix}}' 开头的资产", - "filter-type-asset-type-description": "类型为 '{{assetType}}' 的资产", - "filter-type-device-search-query": "设备搜索查询", - "filter-type-device-search-query-description": "类型为 {{deviceTypes}} 且具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的设备", - "filter-type-device-type": "设备类型", - "filter-type-device-type-and-name-description": "类型为 '{{deviceType}}' 且以 '{{prefix}}' 开头的设备", - "filter-type-device-type-description": "类型为 '{{deviceType}}' 的设备", - "filter-type-edge-type": "边缘类型", - "filter-type-edge-type-description": "类型为 '{{edgeType}}' 的边缘", - "filter-type-edge-type-and-name-description": "类型为 '{{edgeType}}' 且以 '{{prefix}}' 开头的边缘", - "filter-type-edge-search-query": "边缘搜索查询", - "filter-type-edge-search-query-description": "类型为 {{edgeTypes}} 且具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的边缘", - "filter-type-entity-list": "实体列表", - "filter-type-entity-name": "实体名称", - "filter-type-entity-view-search-query": "实体视图搜索查询", - "filter-type-entity-view-search-query-description": "类型为 {{entityViewTypes}} 且具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的实体视图", - "filter-type-entity-view-type": "实体视图类型", - "filter-type-entity-view-type-and-name-description": "类型为 {{entityView}}' 且以 '{{prefix}}' 开头的实体视图", - "filter-type-entity-view-type-description": "类型为 '{{entityView}}' 的实体视图", - "filter-type-relations-query": "关联查询", - "filter-type-relations-query-description": "具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的 {{entities}} ", - "filter-type-required": "筛选器类型必填。", - "filter-type-single-entity": "单个实体", - "filter-type-state-entity": "仪表板状态实体", - "filter-type-state-entity-description": "仪表板实体令牌状态参数", - "last-level-relation": "仅获取最后一级关联", - "max-relation-level": "最大关联层级", - "name": "别名", - "name-required": "别名必填", - "no-entity-filter-specified": "没有指定实体筛选器", - "resolve-multiple": "解析为多实体", - "root-entity": "根实体", - "root-state-entity": "使用仪表板状态实体作为根实体", - "state-entity": "仪表板状态实体", - "state-entity-parameter-name": "状态实体参数名称", - "unlimited-level": "不限层级" - }, - "api-usage": { - "api-usage": "Api 使用统计", - "data-points": "数据点", - "data-points-storage-days": "存储点天", - "email": "Email", - "email-messages": "Email messages", - "email-messages-daily-activity": "Email messages daily activity", - "email-messages-hourly-activity": "Email messages hourly activity", - "email-messages-monthly-activity": "Email messages monthly activity", - "exceptions": "Exceptions", - "executions": "Executions", - "javascript": "JavaScript", - "javascript-executions": "JavaScript executions", - "javascript-functions": "JavaScript functions", - "javascript-functions-daily-activity": "JavaScript functions daily activity", - "javascript-functions-hourly-activity": "JavaScript functions hourly activity", - "javascript-functions-monthly-activity": "JavaScript functions monthly activity", - "latest-error": "最新错误", - "messages": "消息", - "permanent-failures": "${entityName} 永久性故障", - "permanent-timeouts": "${entityName} 永久超时", - "processing-failures": "${entityName} 处理失败", - "processing-failures-and-timeouts": "处理失败和超时", - "processing-timeouts": "${entityName} 处理超时", - "queue-stats": "队列统计信息", - "rule-chain": "规则链", - "rule-engine": "规则引擎", - "rule-engine-daily-activity": "Rule Engine daily activity", - "rule-engine-executions": "Rule Engine executions", - "rule-engine-hourly-activity": "Rule Engine hourly activity", - "rule-engine-monthly-activity": "Rule Engine monthly activity", - "rule-engine-statistics": "规则引擎统计信息", - "rule-node": "规则节点", - "sms": "SMS", - "sms-messages": "短信", - "sms-messages-daily-activity": "SMS messages daily activity", - "sms-messages-hourly-activity": "SMS messages hourly activity", - "sms-messages-monthly-activity": "SMS messages monthly activity", - "successful": "${entityName} 成功", - "telemetry": "遥测数据", - "telemetry-persistence": "Telemetry persistence", - "telemetry-persistence-daily-activity": "Telemetry persistence daily activity", - "telemetry-persistence-hourly-activity": "Telemetry persistence hourly activity", - "telemetry-persistence-monthly-activity": "Telemetry persistence monthly activity", - "transport": "Transport", - "transport-daily-activity": "Transport daily activity", - "transport-data-points": "传输数据点", - "transport-hourly-activity": "Transport hourly activity", - "transport-messages": "传输消息", - "transport-monthly-activity": "Transport monthly activity", - "view-details": "查看详细信息", - "view-statistics": "查看统计信息", - "alarm":"告警", - "alarms-created": "创建告警数", - "notifications": "通知" - }, - "asset": { - "add": "添加资产", - "add-asset-text": "添加新资产", - "any-asset": "任何资产", - "asset": "资产", - "asset-details": "资产详情", - "asset-file": "资产档案", - "asset-public": "资产公开", - "asset-required": "资产必填", - "asset-type": "资产类型", - "asset-type-list-empty": "资产类型未选择。", - "asset-type-required": "资产类型必填。", - "asset-types": "资产类型", - "assets": "资产", - "assign-asset-to-customer": "将资产分配给客户", - "assign-asset-to-customer-text": "请选择要分配给客户的资产", - "assign-asset-to-edge-text": "请选择要分配给边缘的资产", - "assign-asset-to-edge-title": "将资产分配给边缘", - "assign-assets": "分配资产", - "assign-assets-text": "分配 { count, plural, 1 {# 个资产} other {# 个资产} } 给客户", - "assign-new-asset": "分配新资产", - "assign-to-customer": "分配给客户", - "assign-to-customer-text": "请选择客户以分配资产", - "assignedToCustomer": "分配客户", - "copyId": "复制资产ID", - "delete": "删除资产", - "delete-asset-text": "请注意:确认后,资产及其所有相关数据将不可恢复。", - "delete-asset-title": "确定要删除资产 '{{assetName}}'吗?", - "delete-assets": "删除资产", - "delete-assets-action-title": "删除 { count, plural, 1 {# 个资产} other {# 个资产} }", - "delete-assets-text": "请注意:确认后,所有选定的资产将被删除,所有相关的数据将不可恢复。", - "delete-assets-title": "确定要删除 { count, plural, 1 {# 个资产} other {# 个资产} }吗?", - "description": "说明", - "details": "详情", - "enter-asset-type": "输入资产类型", - "events": "事件", - "idCopiedMessage": "资产ID已经复制到粘贴板", - "import": "导入资产", - "label": "标签", - "make-private": "资产设为私有", - "make-private-asset-text": "确认后,资产及其所有数据将被私有化,无法被他人访问。", - "make-private-asset-title": "确定要将资产 '{{assetName}}' 设为私有吗?", - "make-public": "资产设为公开", - "make-public-asset-text": "确认后,资产及其所有数据将被公开并被他人访问。", - "make-public-asset-title": "确定要将资产 '{{assetName}}' 设为公开吗?", - "management": "资产管理", - "name": "名称", - "name-required": "名称必填。", - "name-starts-with": "资产名称以此开头", - "no-asset-types-matching": "未找到匹配 '{{entitySubtype}}' 的资产类型。", - "no-assets-matching": "未找到匹配 '{{entity}}' 的资产。", - "no-assets-text": "未找到资产", - "public": "公开", - "search": "查找资产", - "select-asset": "选择资产", - "select-asset-type": "选择资产类型", - "selected-assets": "已选择 { count, plural, 1 {# 个资产} other {# 个资产} }", - "type": "类型", - "type-required": "类型必填。", - "unassign-asset": "未分配资产", - "unassign-asset-from-edge": "取消分配边缘", - "unassign-asset-from-edge-text": "确认后,所有选定的资产将被分配,边缘无法访问。", - "unassign-asset-from-edge-title": "确定要取消对'{{assetName}}'资产的分配吗?", - "unassign-asset-text": "确认后,资产将未分配,客户无法访问。", - "unassign-asset-title": "确定要取消对'{{assetName}}'资产的分配吗?", - "unassign-assets": "取消分配资产", - "unassign-assets-action-title": "从客户处取消分配 { count, plural, 1 {# 个资产} other {# 个资产} }", - "unassign-assets-text": "确认后,所有选定的资产将被分配,客户无法访问。", - "unassign-assets-title": "确定要取消分配 { count, plural, 1 {# 个资产} other {# 个资产} }吗?", - "unassign-from-customer": "取消分配客户", - "unassign-assets-from-edge": "取消分配边缘", - "unassign-assets-from-edge-text": "确认后,所有选定的资产将被分配,边缘无法访问。", - "unassign-assets-from-edge-title": "确定要取消分配 { count, plural, 1 {1 资产} other {# 个资产} }吗?", - "view-assets": "查看资产" - }, - "attribute": { - "add": "添加属性", - "add-to-dashboard": "添加到仪表板", - "add-widget-to-dashboard": "将部件添加到仪表板", - "attributes": "属性", - "attributes-scope": "设备属性范围", - "delete-attributes": "删除属性", - "delete-attributes-text": "注意,确认后所有选中的属性都会被删除。", - "delete-attributes-title": "确定要删除 { count, plural, 1 {# 个属性} other {# 个属性} }吗?", - "enter-attribute-value": "输入属性值", - "key": "键名", - "key-required": "属性键必填。", - "last-update-time": "最后更新时间", - "latest-telemetry": "最新遥测数据", - "next-widget": "下一个部件", - "no-attributes-text": "未找到属性", - "no-telemetry-text": "未找到遥测数据", - "prev-widget": "上一个部件", - "scope-client": "客户端属性", - "scope-latest-telemetry": "最新遥测数据", - "scope-server": "服务端属性", - "scope-shared": "共享属性", - "selected-attributes": "已选择{ count, plural, 1 {# 个属性} other {# 个属性} }", - "selected-telemetry": "已选择 { count, plural, 1 {# telemetry unit} other {# telemetry units} }", - "show-on-widget": "在部件上显示", - "value": "数值", - "value-required": "属性值必填。", - "widget-mode": "部件模式" - }, - "audit-log": { - "action-data": "活动数据", - "audit": "审计", - "audit-log-details": "审计日志详情", - "audit-logs": "审计日志", - "clear-search": "清空查找", - "details": "详情", - "entity-name": "实体名称", - "entity-type": "实体类型", - "failure-details": "失败详情", - "no-audit-logs-prompt": "未找到日志", - "search": "查找审计日志", - "status": "状态", - "status-failure": "失败", - "status-success": "成功", - "timestamp": "时间戳", - "type": "类型", - "type-activated": "激活", - "type-added": "添加", - "type-alarm-ack": "已确认", - "type-alarm-clear": "已清除", - "type-assigned-from-tenant": "从租户分配", - "type-assigned-to-customer": "分配给客户", - "type-assigned-to-edge": "分配给边缘", - "type-assigned-to-tenant": "分配给租户", - "type-attributes-deleted": "删除属性", - "type-attributes-read": "读取属性", - "type-attributes-updated": "更新属性", - "type-credentials-read": "读取凭据", - "type-credentials-updated": "更新凭据", - "type-deleted": "删除", - "type-lockout": "锁定", - "type-login": "登录", - "type-logout": "注销", - "type-provision-failure": "设备预配置失败", - "type-provision-success": "设备已预配置", - "type-relation-add-or-update": "关联已更新", - "type-relation-delete": "关联已删除", - "type-relations-delete": "删除所有关联", - "type-rpc-call": "RPC调用", - "type-suspended": "暂停", - "type-timeseries-deleted": "遥测数据已删除", - "type-timeseries-updated": "遥测数据已更新", - "type-unassigned-from-customer": "未分配给客户", - "type-unassigned-from-edge": "未分配给边缘", - "type-updated": "更新", - "user": "用户" - }, - "common": { - "created-time": "创建时间", - "enter-password": "输入密码", - "enter-search": "输入检索条件", - "enter-username": "输入用户名", - "loading": "正在加载中...", - "password": "密码", - "username": "用户名", - "proceed": "继续", - "open-details-page": "打开详情页" - }, - "confirm-on-exit": { - "html-message": "有未保存的更改。
确定要离开此页面吗?", - "message": "有未保存的更改,确定要离开此页吗?", - "title": "未保存的更改" - }, - "contact": { - "address": "地址", - "address2": "地址二", - "city": "城市", - "country": "国家", - "email": "电子邮件", - "no-address": "无地址", - "phone": "电话", - "postal-code": "邮政编码", - "postal-code-invalid": "只允许数字。", - "state": "州" - }, - "content-type": { - "binary": "Binary (Base64)", - "json": "Json", - "text": "Text" - }, - "custom": { - "widget-action": { - "action-cell-button": "动作单元格按钮", - "element-click": "点击HTML元素时", - "marker-click": "点击标记时", - "node-selected": "选择节点时", - "pie-slice-click": "点击切片时", - "polygon-click": "单击多边形", - "row-click": "点击行时", - "row-double-click": "双击行时", - "tooltip-tag-action": "Tooltip tag action" - } - }, - "customer": { - "add": "添加客户", - "add-customer-text": "添加新客户", - "assets": "客户资产", - "copyId": "复制客户ID", - "customer": "客户", - "customer-details": "客户详情", - "customer-required": "客户必填", - "customers": "客户", - "dashboard": "客户仪表板", - "dashboards": "客户仪表板", - "default-customer": "默认客户", - "default-customer-required": "为了调试租户级别上的仪表板,需要默认客户。", - "delete": "删除此客户", - "delete-customer-text": "请注意:确认后,客户及其所有相关数据将不可恢复。", - "delete-customer-title": "确定要删除客户'{{customerTitle}}'吗?", - "delete-customers-action-title": "删除 { count, plural, 1 {# 个客户} other {# 个客户} }", - "delete-customers-text": "请注意:确认后,所有选定的客户将被删除,所有相关数据将不可恢复。", - "delete-customers-title": "确定要删除 { count, plural, 1 {# 个客户} other {# 个客户} }吗?", - "description": "说明", - "details": "详情", - "devices": "客户设备", - "edges": "客户边缘实例", - "entity-views": "客户实体视图", - "events": "事件", - "idCopiedMessage": "客户ID已复制到粘贴板", - "manage-assets": "管理资产", - "manage-customer-assets": "管理客户资产", - "manage-customer-dashboards": "管理客户仪表板", - "manage-customer-devices": "管理客户设备", - "manage-customer-edges": "管理客户边缘", - "manage-customer-users": "管理客户用户", - "manage-dashboards": "管理仪表板", - "manage-devices": "管理设备", - "manage-edges": "管理边缘", - "manage-public-assets": "管理公共资产", - "manage-public-dashboards": "管理公共仪表板", - "manage-public-devices": "管理公共设备", - "manage-public-edges": "管理公共边缘", - "manage-users": "管理用户", - "management": "客户管理", - "no-customers-matching": "未找到匹配 '{{entity}}' 的客户。", - "no-customers-text": "未找到客户", - "public-assets": "公共资产", - "public-dashboards": "公共仪表板", - "public-devices": "公共设备", - "public-entity-views": "公共实体视图", - "public-edges": "公共边缘", - "search": "查找客户", - "select-customer": "选择客户", - "select-default-customer": "选择默认的客户", - "selected-customers": "已选择 { count, plural, 1 {# 个客户} other {# 个客户} }", - "title": "标题", - "title-required": "标题必填。" - }, - "dashboard": { - "add": "添加仪表板", - "add-dashboard-text": "添加新的仪表板", - "add-state": "添加仪表板状态", - "add-widget": "添加新的部件", - "alias-resolution-error-title": "仪表板别名配置错误", - "assign-dashboard-to-customer": "将仪表板分配给客户", - "assign-dashboard-to-customer-text": "请选择要分配给客户的仪表板", - "assign-dashboard-to-edge": "将仪表板分配给边缘", - "assign-dashboard-to-edge-text": "请选择要分配给边缘的仪表板", - "assign-dashboard-to-edge-title": "将仪表板分配给边缘", - "assign-dashboards": "分配仪表板", - "assign-dashboards-text": "分配 { count, plural, 1 {# 个仪表板} other {# 个仪表板} } 给客户", - "assign-new-dashboard": "分配新的仪表板", - "assign-to-customer": "分配给客户", - "assign-to-customer-text": "请选择客户分配仪表板", - "assign-to-customers": "将仪表板分配给客户", - "assign-to-customers-text": "请选择客户指定仪表板", - "assigned-customers": "已分配的客户", - "assignedToCustomer": "分配给客户", - "assignedToCustomers": "分配给客户", - "autofill-height": "自动填充布局高度", - "background-color": "背景颜色", - "background-image": "背景图片", - "background-size-mode": "背景大小模式", - "cannot-upload-file": "无法上传文件", - "close-toolbar": "关闭工具栏", - "columns-count": "列数", - "columns-count-required": "需要列数。", - "configuration-error": "配置错误", - "copy-public-link": "复制公共链接", - "create-new": "创建新的仪表板", - "create-new-dashboard": "创建新的仪表板", - "create-new-widget": "创建新部件", - "dashboard": "仪表板", - "dashboard-details": "仪表板详情", - "dashboard-file": "仪表板文件", - "dashboard-import-missing-aliases-title": "配置导入仪表板使用的别名", - "dashboard-logo-image": "仪表板 Logo 图片", - "dashboard-required": "仪表板必填。", - "dashboards": "仪表板库", - "delete": "删除仪表板", - "delete-dashboard-text": "请注意:确认后,仪表板及其所有相关数据将不可恢复。", - "delete-dashboard-title": "确定要删除仪表板 '{{dashboardTitle}}'吗?", - "delete-dashboards": "删除仪表板", - "delete-dashboards-action-title": "删除 { count, plural, 1 {# 个仪表板} other {# 个仪表板} }", - "delete-dashboards-text": "请注意:确认后,所有选定的仪表板将被删除,所有相关数据将不可恢复。", - "delete-dashboards-title": "确定要删除 { count, plural, 1 {# 个仪表板} other {# 个仪表板} }吗?", - "delete-state": "删除仪表板状态", - "delete-state-text": "确定要删除仪表板状态 '{{stateName}}' 吗?", - "delete-state-title": "删除仪表板状态", - "description": "说明", - "details": "详情", - "display-dashboard-export": "显示导出", - "display-dashboard-logo": "在仪表板全屏模式下显示 Logo", - "display-dashboard-timewindow": "显示时间窗口", - "display-dashboards-selection": "显示仪表板选项", - "display-entities-selection": "显示实体选项", - "display-filters": "显示筛选器", - "display-title": "显示仪表板标题", - "drop-image": "拖拽图像或单击以选择要上传的文件。", - "maximum-upload-file-size": "最大上传文件大小: {{ size }}", - "edit-state": "仪表板状态编辑", - "export": "导出仪表板", - "export-failed-error": "无法导出仪表板: {{error}}", - "hide-details": "隐藏详情", - "home-dashboard": "首页仪表板", - "home-dashboard-hide-toolbar": "隐藏首页仪表板工具栏", - "horizontal-margin": "水平边距", - "horizontal-margin-required": "需要水平边距值。", - "import": "导入仪表板", - "import-widget": "导入部件", - "invalid-aliases-config": "无法找到与某些别名筛选器匹配的任何设备。
请联系您的管理员以解决此问题。", - "invalid-dashboard-file-error": "无法导入仪表板: 仪表板数据结构无效。", - "invalid-widget-file-error": "无法导入窗口部件: 窗口部件数据结构无效。", - "is-root-state": "根状态", - "make-private": "仪表板设为私有", - "make-private-dashboard": "仪表板设为私有", - "make-private-dashboard-text": "确认后,仪表板将被设为私有,不能被其他人访问。", - "make-private-dashboard-title": "确定要将仪表板 '{{dashboardTitle}}' 设为私有吗?", - "make-public": "仪表板设为公开", - "manage-assigned-customers": "管理已分配的客户", - "manage-states": "仪表板状态管理", - "management": "仪表板管理", - "margin-required": "边距值必填。", - "max-columns-count-message": "只允许最多1000列", - "max-horizontal-margin-message": "只允许50作为最大水平边距值。", - "max-margin-message": "仅允许50作为最大边距值。", - "max-mobile-row-height-message": "移动端行高距最多200px。", - "max-vertical-margin-message": "只允许50作为最大垂直边距值。", - "min-columns-count-message": "只允许最少10列", - "min-horizontal-margin-message": "只允许0作为最小水平边距值。", - "min-margin-message": "最小边距值只允许为0。", - "min-mobile-row-height-message": "移动端行高距至少5px。", - "min-vertical-margin-message": "只允许0作为最小垂直边距值。", - "mobile-layout": "移动端布局设置", - "mobile-row-height": "移动端行高距(px)", - "mobile-row-height-required": "移动端行高距必填。", - "new-dashboard-title": "新仪表板标题", - "no-dashboards-matching": "未找到匹配 '{{entity}}' 的仪表板。", - "no-dashboards-text": "未找到仪表板", - "no-image": "无图像选择", - "no-states-text": "未找到状态", - "no-widgets": "没有配置部件", - "open-dashboard": "打开仪表板", - "open-toolbar": "打开仪表板工具栏", - "public": "公开", - "public-dashboard-notice": "提示: 不要忘记将相关设备公开以访问其数据。", - "public-dashboard-text": "仪表板 {{dashboardTitle}} 已被公开,可通过如下 链接访问:", - "public-dashboard-title": "仪表板现已公开", - "public-link": "公共链接", - "public-link-copied-message": "仪表板的公共链接已被复制到剪贴板", - "search": "查找仪表板", - "search-states": "仪表板状态检索", - "select-dashboard": "选择仪表板", - "select-devices": "选择设备", - "select-existing": "选择现有仪表板", - "select-state": "选择目标状态", - "select-widget-subtitle": "可用的部件类型列表", - "select-widget-title": "选择部件", - "select-widget-value": "{{title}}: 选择部件", - "selected-dashboards": "已选择 { count, plural, 1 {# 个仪表盘} other {# 个仪表盘} }", - "selected-states": "已选择 { count, plural, 1 {# 个仪表板状态} other {# 个仪表板状态} }", - "set-background": "设置背景", - "settings": "设置", - "show-details": "显示详情", - "socialshare-text": "'{{dashboardTitle}}' 由Thingsboard提供支持", - "socialshare-title": "'{{dashboardTitle}}' 由Thingsboard提供支持", - "state": "仪表板状态", - "state-controller": "状态控制", - "state-id": "状态ID", - "state-id-exists": "仪表板状态ID已经存在。", - "state-id-required": "仪表板状态ID必填。", - "state-name": "名称", - "state-name-required": "仪表板状态名必填。", - "states": "仪表板状态", - "title": "标题", - "image": "仪表板图片", - "mobile-app-settings": "移动端应用设置", - "mobile-order": "移动端应用中的仪表板排序", - "mobile-hide": "在移动端应用中隐藏仪表板", - "title-color": "标题颜色", - "title-required": "标题必填。", - "toolbar-always-open": "工具栏常驻", - "unassign-dashboard": "取消分配仪表板", - "unassign-dashboard-from-edge-text": "确认后,所有选定的仪表板将被取消分配,边缘将无法访问。", - "unassign-dashboard-text": "确认后,面板将被取消分配,客户将无法访问。", - "unassign-dashboard-title": "确定要取消分配仪表板 '{{dashboardTitle}}'吗?", - "unassign-dashboards": "取消分配仪表板", - "unassign-dashboards-action-text": "取消分配 { count, plural, 1 {# 个仪表板} other {# 个仪表板} } 给客户", - "unassign-dashboards-action-title": "取消分配此客户 { count, plural, 1 {# 个仪表板} other {# 个仪表板} }", - "unassign-dashboards-text": "确认后,所有选定的仪表板将被取消分配,客户将无法访问。", - "unassign-dashboards-title": "确定要取消分配仪表板 { count, plural, 1 {# 个仪表板} other {# 个仪表板} } 吗?", - "unassign-dashboards-from-edge-text": "确认后,所有选定的仪表板将被取消分配,边缘将无法访问。", - "unassign-dashboards-from-edge-title": "确定要取消分配仪表板 { count, plural, 1 {# 个仪表板} other {# 个仪表板} } 吗?", - "unassign-from-customer": "取消分配客户", - "unassign-from-customers": "客户未分配仪表板", - "unassign-from-customers-text": "请选择从仪表板中取消分配的客户", - "vertical-margin": "垂直边距", - "vertical-margin-required": "需要垂直边距值。", - "view-dashboards": "查看仪表板", - "widget-file": "部件文件", - "widget-import-missing-aliases-title": "配置导入的窗口部件使用的别名", - "widgets-margins": "部件间边距" - }, - "datakey": { - "advanced": "高级", - "alarm": "告警字段", - "alarm-fields-required": "告警字段必填。", - "alarm-fields-timeseries-or-attributes-required": "告警字段或实体 Timeseries/属性必填。", - "attributes": "属性", - "color": "颜色", - "configuration": "数据键配置", - "data-generation-func": "数据生成功能", - "decimals": "小数位数", - "entity-field": "实体字段", - "function-types": "函数类型", - "function-types-required": "需要函数类型。", - "label": "标签", - "maximum-function-types": "最多允许 { count, plural, 1 {# 个函数类型} other {# 个函数类型} }", - "maximum-timeseries-or-attributes": "最多允许 { count, plural, 1 {# 个 timeseries/属性。} other {# 个 timeseries/属性。} }", - "prev-orig-value-description": "先前的原始值;", - "prev-value-description": "上一次函数调用的结果;", - "settings": "设置", - "time-description": "当前值的时间戳;", - "time-prev-description": "上一个值的时间戳;", - "timeseries": "Timeseries", - "timeseries-or-attributes-required": "实体 Timeseries/属性必填。", - "timeseries-required": "实体 Timeseries 必填。", - "units": "单位符号", - "use-data-post-processing-func": "使用数据后处理功能", - "value-description": "当前值;" - }, - "datasource": { - "add-datasource-prompt": "请添加数据源", - "label": "标签", - "name": "名称", - "type": "数据源类型" - }, - "datetime": { - "date-from": "日期从", - "date-to": "日期到", - "time-from": "时间从", - "time-to": "时间到" - }, - "details": { - "details": "详情", - "edit-json": "编辑JSON", - "edit-mode": "编辑模式", - "toggle-edit-mode": "切换编辑模式" - }, - "asset-profile": { - "asset-profile": "资产配置", - "asset-profiles": "资产配置", - "all-asset-profiles": "全部", - "add": "添加资产配置", - "edit": "编辑资产配置", - "asset-profile-details": "资产配置详情", - "no-asset-profiles-text": "未找到资产配置", - "search": "搜索资产配置", - "selected-asset-profiles": "已选择 { count, plural, 1 {1 个资产配置} other {# 个资产配置} }", - "no-asset-profiles-matching": "未找到与 '{{entity}}' 匹配的资产配置。", - "asset-profile-required": "资产配置必填", - "idCopiedMessage": "资产配置ID已复制到剪贴板", - "set-default": "设为默认资产配置", - "delete": "删除资产配置", - "copyId": "复制资产配置ID", - "name-max-length": "名称长度必须少于256个字符", - "new-device-profile-name": "资产配置名称", - "new-device-profile-name-required": "资产配置名称必填。", - "name": "名称", - "name-required": "名称必填。", - "image": "资产配置图片", - "description": "说明", - "default": "默认", - "default-rule-chain": "默认规则链", - "mobile-dashboard": "移动端仪表板", - "mobile-dashboard-hint": "被移动端应用用作资产详情仪表板", - "select-queue-hint": "从下拉列表中选择。", - "delete-asset-profile-title": "确定要删除 '{{assetProfileName}}' 资产配置吗?", - "delete-asset-profile-text": "请注意:确认后,资产配置和所有相关数据将不可恢复。", - "delete-asset-profiles-title": "确定要删除 { count, plural, 1 {1 个资产配置} other {# 个资产配置} }吗?", - "delete-asset-profiles-text": "请注意:确认后,所有选定的资产配置将被删除,所有相关数据将不可恢复。", - "set-default-asset-profile-title": "确定要将 '{{assetProfileName}}' 设为默认资产配置吗?", - "set-default-asset-profile-text": "确认后,资产配置将被标记为默认,并将用于未指定配置的新资产。", - "no-asset-profiles-found": "未不到资产配置。", - "create-new-asset-profile": "创建一个新的!", - "create-asset-profile": "创建资产配置", - "import": "导入资产配置", - "export": "导出资产配置", - "export-failed-error": "无法导出资产配置: {{error}}", - "asset-profile-file": "资产配置文件", - "invalid-asset-profile-file-error": "无法导入资产配置:无效的资产配置数据结构。" - }, - "device-profile": { - "add": "添加设备配置", - "add-alarm-rule": "添加告警规则", - "add-alarm-rule-details": "详情模板:", - "add-clear-alarm-rule": "添加清除条件", - "add-create-alarm-rule": "添加创建条件", - "add-create-alarm-rule-prompt": "请添加创建告警规则", - "advanced-settings": "高级设置", - "alarm-details": "告警详细信息", - "alarm-rule-condition": "告警规则条件", - "alarm-rule-details": "详情", - "alarm-rule-relation-types-list": "要传递的关联类型", - "alarm-rule-relation-types-list-hint": "如果未选择传递关联类型,则将不按关联类型过滤而传递告警。", - "alarm-rules": "告警规则", - "alarm-rules-with-count": "告警规则 ({{count}})", - "alarm-severity-required": "告警严重级别必填。", - "alarm-type": "告警类型", - "alarm-type-required": "告警类型必填。", - "alarm-type-unique": "警报类型在设备配置警报规则中必须唯一。", - "all-device-profiles": "全部", - "attributes-proto-schema": "Attributes proto schema", - "attributes-proto-schema-required": "Attributes proto schema 必填。", - "attributes-topic-filter": "Attributes topic filter", - "attributes-topic-filter-required": "Attributes topic 筛选器必填。", - "clear-alarm-rule": "清除告警规则", - "coap-device-payload-type": "CoAP 设备消息 Payload", - "coap-device-type": "CoAP 设备类型", - "coap-device-type-default": "默认", - "coap-device-type-efento": "Efento NB-IoT", - "coap-device-type-required": "CoAP 设备类型必填。", - "condition": "条件", - "condition-duration": "条件持续时间", - "condition-duration-time-unit": "时间单位", - "condition-duration-time-unit-required": "时间单位必填。", - "condition-duration-value": "持续时间值", - "condition-duration-value-pattern": "持续时间值应为整数。", - "condition-duration-value-range": "持续时间值应在1到2147483647之间。", - "condition-duration-value-required": "持续时间值必填。", - "condition-during": "在 {{during}} 期间", - "condition-repeat-times": "重复 { count, plural, 1 {# 次} other {# 次} }", - "condition-repeating-value": "事件计数", - "condition-repeating-value-pattern": "事件计数应为整数。", - "condition-repeating-value-range": "事件计数应在1到2147483647之间。", - "condition-repeating-value-required": "事件计数值必填。", - "condition-type": "条件类型", - "condition-type-duration": "持续时间", - "condition-type-repeating": "重复", - "condition-type-required": "条件类型必填。", - "condition-type-simple": "简单", - "copy-provision-key": "复制预配置密钥名", - "copy-provision-secret": "复制预配置密钥", - "copyId": "复制设备配置 ID", - "create-alarm-pattern": "创建 {{alarmType}} 告警", - "create-alarm-rules": "创建告警规则", - "create-device-profile": "创建设备配置", - "create-new-device-profile": "创建一个新的!", - "default": "默认", - "default-rule-chain": "默认规则链", - "mobile-dashboard": "移动端仪表板", - "mobile-dashboard-hint": "被移动端应用用作设备详情仪表板", - "delete": "删除设备配置", - "delete-device-profile-text": "请注意:确认后,设备配置和所有相关数据将不可恢复。", - "delete-device-profile-title": "确定要删除 '{{deviceProfileName}}' 设备配置吗?", - "delete-device-profiles-text": "请注意:确认后,所有选定的设备配置将被删除,所有相关数据将不可恢复。", - "delete-device-profiles-title": "确定要删除 { count, plural, 1 {# 个设备配置} other {# 个设备配置} }吗?", - "description": "说明", - "device-profile": "设备配置", - "device-profile-details": "设备配置详情", - "device-profile-file": "设备配置文件", - "device-profile-required": "设备配置必填", - "device-profiles": "设备配置", - "device-provisioning": "设备预配置", - "edit": "编辑设备配置", - "edit-alarm-rule": "编辑告警规则", - "edit-alarm-rule-condition": "编辑告警规则条件", - "edit-schedule": "编辑告警日程表", - "enter-alarm-rule-condition-prompt": "请添加告警规则条件", - "export": "导出设备配置", - "export-failed-error": "无法导出设备配置文件: {{error}}", - "idCopiedMessage": "设备配置 ID 已复制到剪贴板", - "import": "导入设备配置", - "invalid-device-profile-file-error": "无法导入设备配置:无效的设备配置数据结构。", - "mqtt-device-payload-type": "MQTT 设备 Payload", - "mqtt-device-payload-type-json": "JSON", - "mqtt-device-payload-type-proto": "Protobuf", - "mqtt-device-topic-filters": "MQTT 设备 Topic 筛选器", - "mqtt-device-topic-filters-unique": "MQTT设备 Topic 筛选器必须唯一。", - "mqtt-payload-type-required": "Payload 类型必填。", - "multi-level-wildcards-hint": "[#]可以替换 topic filter 本身,并且必须是 topic 的最后一个符号。例如:# or v1/devices/me/#。", - "name": "名称", - "name-required": "名称是必需的。", - "new-device-profile-name": "设备配置名称", - "new-device-profile-name-required": "设备配置名称必填。", - "no-alarm-rules": "未配置告警规则", - "no-clear-alarm-rule": "未配置明确条件", - "no-create-alarm-rules": "未配置创建条件", - "no-device-profiles-found": "未找到设备配置。", - "no-device-profiles-matching": "未找到与 '{{entity}}' 匹配的设备配置。", - "no-device-profiles-text": "未找到设备配置", - "not-valid-multi-character": "多级通配符的使用无效", - "not-valid-pattern-topic-filter": "无效的 Topic 筛选器模式", - "not-valid-single-character": "单级通配符的使用无效", - "profile-configuration": "配置", - "propagate-alarm": "传递警报", - "provision-device-key": "预配置设备密钥名", - "provision-device-key-required": "预配置设备密钥名必填。", - "provision-device-secret": "预配置设备密钥", - "provision-device-secret-required": "预配置设备密钥必填。", - "provision-key-copied-message": "预配置密钥名已复制到剪贴板", - "provision-secret-copied-message": "预配置密钥已复制到剪贴板", - "provision-strategy": "预配置策略", - "provision-strategy-check-pre-provisioned": "检查预配置的设备", - "provision-strategy-created-new": "允许创建新设备", - "provision-strategy-disabled": "禁用", - "provision-strategy-required": "预配置策略必填。", - "rpc-request-proto-schema": "RPC 请求 proto schema", - "rpc-request-proto-schema-hint": "RPC 请求消息应始终包含字段:string method = 1; int32 requestId = 2; 和params = 3的任何数据类型。", - "rpc-request-proto-schema-required": "RPC 请求 proto schema 必填。", - "rpc-response-proto-schema": "RPC 响应 proto schema", - "rpc-response-proto-schema-required": "RPC 响应 proto schema 必填。", - "rpc-response-topic-filter": "RPC响应 Topic 筛选器", - "rpc-response-topic-filter-required": "RPC响应 Topic 筛选器必填。", - "schedule": "启用规则:", - "schedule-any-time": "始终启用", - "schedule-custom": "自定义启用", - "schedule-day": { - "friday": "星期五", - "monday": "星期一", - "saturday": "星期六", - "sunday": "星期日", - "thursday": "星期四", - "tuesday": "星期二", - "wednesday": "星期三" - }, - "schedule-days": "天", - "schedule-days-of-week-required": "每周至少选择一天。", - "schedule-specific-time": "定时启用", - "schedule-time": "时间", - "schedule-time-from": "从", - "schedule-time-to": "到", - "schedule-type": "计划程序类型", - "schedule-type-required": "计划类型必填。", - "search": "查找设备配置", - "select-alarm-severity": "选择告警严重性", - "select-queue-hint": "从下拉列表中选择或添加自定义名称。", - "selected-device-profiles": "已选择 { count, plural, 1 {# 个设备配置} other {# 个设备配置} }", - "set-default": "设为默认设备配置", - "set-default-device-profile-text": "确认后,设备配置将被标记为默认,并将用于未指定配置的新设备。", - "set-default-device-profile-title": "确定要将设备配置 '{{deviceProfileName}}' 设为默认值吗?", - "single-level-wildcards-hint": "[+] is suitable for any topic filter level。例如:v1/devices/+/telemetry or +/devices/+/attributes。", - "support-level-wildcards": "支持单[+]和多级[#]通配符。", - "telemetry-proto-schema": "遥测数据 proto schema", - "telemetry-proto-schema-required": "遥测数据 proto schema 必填。", - "telemetry-topic-filter": "遥测数据 topic 筛选器", - "telemetry-topic-filter-required": "遥测数据 topic 筛选器必填。", - "transport-configuration": "传输配置", - "transport-type": "传输方式", - "transport-type-coap-hint": "启用高级 CoAP 传输设置", - "transport-type-default": "默认", - "transport-type-default-hint": "支持基本MQTT、HTTP和CoAP传输", - "transport-type-lwm2m": "LWM2M", - "transport-type-lwm2m-hint": "LWM2M传输类型", - "transport-type-mqtt": "MQTT", - "transport-type-mqtt-hint": "启用高级MQTT传输设置", - "transport-type-required": "传输方式必填。", - "transport-type-snmp-hint": "指定 SNMP 传输配置", - "type": "配置类型", - "type-default": "默认", - "type-required": "配置类型必填。", - "image": "设备配置图片" - }, - "device": { - "access-token": "访问令牌", - "access-token-invalid": "访问令牌长度必须为1到32个字符。", - "access-token-required": "访问令牌必填", - "accessTokenCopiedMessage": "设备访问令牌已复制到剪贴板", - "add": "添加设备", - "add-alias": "添加设备别名", - "add-device-text": "添加新设备", - "alias": "别名", - "alias-required": "设备别名必填。", - "aliases": "设备别名", - "any-device": "任意设备", - "assign-device-to-customer": "将设备分配给客户", - "assign-device-to-customer-text": "请选择要分配给客户的设备", - "assign-device-to-edge-text":"请选择要分配给边缘的设备", - "assign-device-to-edge-title": "将设备分配给边缘", - "assign-devices": "分配设备", - "assign-devices-text": "将 {count,plural,1 {# 个设备} other {# 个设备} }分配给客户", - "assign-new-device": "分配新设备", - "assign-to-customer": "分配给客户", - "assign-to-customer-text": "请选择客户分配设备", - "assignedToCustomer": "分配给客户", - "client-id": "客户端ID", - "client-id-or-user-name-necessary": "客户端ID和/或用户名是必需的", - "client-id-pattern": "包含无效字符。", - "configure-alias": "配置 '{{alias}}' 别名", - "copy-mqtt-authentication": "复制MQTT凭据", - "copyAccessToken": "复制访问令牌", - "copyId": "复制设备ID", - "create-new-alias": "创建一个新的!", - "create-new-key": "创建一个新的!", - "credentials": "凭据", - "credentials-type": "凭据类型", - "delete": "删除设备", - "delete-device-text": "请注意:确认后,设备及其所有相关数据将不可恢复。", - "delete-device-title": "确定要删除设备的{{deviceName}}吗?", - "delete-devices": "删除设备", - "delete-devices-action-title": "删除 {count,plural,1 {# 个设备} other {# 个设备} }", - "delete-devices-text": "请注意:确认后,所有选定的设备将被删除,所有相关数据将不可恢复。", - "delete-devices-title": "确定要删除{count,plural,1 {# 个设备} other {# 个设备} } 吗?", - "description": "说明", - "details": "详情", - "device": "设备", - "device-alias": "设备别名", - "device-configuration": "设备配置", - "device-credentials": "设备凭据", - "device-details": "设备详细信息", - "device-file": "设备文件", - "device-list": "设备列表", - "device-list-empty": "没有被选中的设备", - "device-name-filter-no-device-matched": "未找到以'{{device}}' 开头的设备。", - "device-name-filter-required": "设备名称筛选器必填。", - "device-public": "设备公开", - "device-required": "设备必填", - "device-type": "设备类型", - "device-type-list-empty": "未选择设备类型", - "device-type-required": "设备类型必填。", - "device-types": "设备类型", - "devices": "设备", - "duplicate-alias-error": "找到重复别名 '{{alias}}'。
设备别名必须是唯一的。", - "enter-device-type": "输入设备类型", - "events": "事件", - "idCopiedMessage": "设备ID已复制到剪贴板", - "import": "导入设备", - "is-gateway": "是否网关", - "label": "标签", - "make-private": "私有", - "make-private-device-text": "确认后,设备及其所有数据将被设为私有,不被其他人访问。", - "make-private-device-title": "确定要将设备 '{{deviceName}}' 设为私有吗?", - "make-public": "公开", - "make-public-device-text": "确认后,设备及其所有数据将被设为公开并可被其他人访问。", - "make-public-device-title": "确定要将设备 '{{deviceName}}' 设为公开吗?", - "manage-credentials": "管理凭据", - "management": "设备管理", - "mqtt-authentication-copied-message": "设备MQTT身份验证已复制到剪贴板", - "name": "名称", - "name-required": "名称必填。", - "name-starts-with": "名称前缀", - "no-alias-matching": "'{{alias}}' 未找到。", - "no-aliases-found": "未找到别名。", - "no-device-types-matching": "未找到匹配 '{{entitySubtype}}' 的设备类型。", - "no-devices-matching": "未找到与 '{{entity}}' 匹配的设备。", - "no-devices-text": "未找到设备", - "no-key-matching": "'{{key}}' 未找到。", - "no-keys-found": "未找到密钥。", - "overwrite-activity-time": "覆盖已连接设备的活动时间", - "password": "密码", - "public": "公开", - "remove-alias": "删除设备别名", - "search": "查找设备", - "secret": "密钥", - "secret-required": "密钥必填", - "select-device": "选择设备", - "select-device-type": "选择设备类型", - "selected-devices": "已选择 { count, plural, 1 {# 个设备} other {# 个设备} }", - "transport-configuration": "传输配置", - "unable-delete-device-alias-text": "设备别名 '{{deviceAlias}}' 不能够被删除,因为它被下列部件所使用:
{{widgetsList}}", - "unable-delete-device-alias-title": "无法删除设备别名", - "unassign-device": "取消分配设备", - "unassign-device-text": "确认后,设备将被取消分配,客户将无法访问。", - "unassign-device-title": "确定要取消分配设备 '{{deviceName}}' 吗?", - "unassign-device-from-edge-text": "确认后,设备将被取消分配,边缘将无法访问。", - "unassign-device-from-edge-title": "确定要取消分配设备 '{{deviceName}}' 吗?", - "unassign-devices": "取消分配设备", - "unassign-devices-action-title": "取消分配此客户 {count,plural,1 {# 个设备} other {# 个设备} }", - "unassign-devices-from-edge": "取消分配边缘", - "unassign-devices-from-edge-text": "确认后,设备将被取消分配,边缘将无法访问。", - "unassign-devices-from-edge-title": "确定要取消分配 { count, plural, 1 {1 个设备} other {# 个设备} } 吗?", - "unassign-devices-text": "确认后,所有选定的设备将被取消分配,并且客户将无法访问。", - "unassign-devices-title": "确定要取消分配 {count,plural,1 {# 个设备} other {# 个设备} } 吗?", - "unassign-from-customer": "取消分配客户", - "use-device-name-filter": "使用筛选器", - "user-name": "用户名", - "user-name-required": "用户名必填。", - "view-credentials": "查看凭据", - "view-devices": "查看设备", - "wizard": { - "add-credentials": "添加凭据", - "customer-to-assign-device": "客户分配设备", - "device-details": "设备详细信息", - "device-wizard": "设备向导", - "existing-device-profile": "选择已有设备配置", - "new-device-profile": "新建设备配置", - "specific-configuration": "指定配置" - } - }, - "dialog": { - "close": "关闭对话框" - }, - "direction": { - "column": "列", - "row": "排" - }, - "edge": { - "add": "增加边缘", - "add-edge-text": "增加新的边缘", - "any-edge": "任何边缘", - "assets": "边缘资产", - "assign-edge-to-customer": "分配边缘给客户", - "assign-edge-to-customer-text": "请选择需要分配给边缘的客户", - "assign-new-edge": "分配新边缘", - "assign-to-customer": "分配给客户", - "assign-to-customer-text": "请选择需要分配给边缘的客户", - "assigned-to-customer": "分配给: {{customerTitle}}", - "assignedToCustomer": "分配给客户", - "copy-edge-key": "复制边缘键", - "copy-edge-secret": "复制边缘密钥", - "copy-id": "复制边缘编号", - "dashboard": "边缘仪表板", - "dashboards": "边缘仪表板", - "delete": "删除边缘", - "delete-edge-text": "当心, 确认后,边缘以及所有关联数据将不可恢复。", - "delete-edge-title": "确定删除边缘 '{{edgeName}}'吗?", - "delete-edges-text": "当心, 确认后,选定的边缘以及所有关联数据将不可恢复。", - "delete-edges-title": "确定删除 { count, plural, 1 {1 个边缘} other {# 个边缘} }吗?", - "deployed": "已部署", - "description": "说明", - "details": "详情", - "devices": "边缘设备", - "downlinks": "下行", - "edge": "边缘", - "edge-assets": "边缘资产", - "edge-dashboards": "边缘仪表板", - "edge-details": "边缘详情", - "edge-devices": "边缘设备", - "edge-entity-views": "边缘实体视图", - "edge-file": "边缘文件", - "edge-instances": "边缘实例", - "edge-key": "边缘键", - "edge-key-copied-message": "边缘键已经被复制到剪切板", - "edge-public": "边缘公开", - "edge-required": "边缘必填。", - "edge-rulechains": "边缘规则链", - "edge-secret": "边缘密钥", - "edge-secret-copied-message": "边缘密钥已经被复制到剪切板", - "edge-type": "边缘类型", - "edge-type-list-empty": "没有选择边缘类型。", - "edge-type-required": "边缘类型必填。", - "edge-types": "边缘类型", - "enter-edge-type": "输入边缘类型", - "entity-id": "实体编号", - "entity-views": "边缘实体视图", - "event-action": "事件行动", - "events": "事件", - "id-copied-message": "边缘编号已经复制到剪切板", - "import": "导入边缘", - "label": "标签", - "label-max-length": "标签长度必须少于256个字符", - "load-entity-error": "加载数据失败,实体已经被删除。", - "make-private": "私有", - "make-private-edge-text": "确认后,边缘及其所有数据将被设为私有,不被其他人访问。", - "make-private-edge-title": "确定要将边缘 '{{edgeName}}' 设为私有吗?", - "make-public": "公开", - "make-public-edge-text": "确认后,边缘及其所有数据将被设为公开并可被其他人访问。", - "make-public-edge-title": "确定要将边缘 '{{edgeName}}' 设为公开吗?", - "management": "边缘管理", - "missing-related-rule-chains-text": "分配给边缘的规则链使用规则节点将消息转发给未分配给当前边缘的规则链。

缺少的规则链列表:
{{missingRuleChains}}", - "missing-related-rule-chains-title": "边缘缺少关联规则链", - "name": "名称", - "name-max-length": "名称长度必须少于256个字符", - "name-required": "名称必填。", - "name-starts-with": "边缘名称前缀", - "no-downlinks-prompt": "未找到下行", - "no-edge-types-matching": "未找到匹配的边缘类型 '{{entitySubtype}}'。", - "no-edges-matching": "未找到匹配的边缘 '{{entity}}'。", - "no-edges-text": "未找到边缘", - "pending": "待定", - "public": "公开", - "rulechain-templates": "规则链模版", - "rulechains": "规则链", - "search": "搜索边缘", - "select-edge-type": "选择边缘类型", - "selected-edges": "{ count, plural, 1 {1 个边缘} other {# 个边缘} } 被选中", - "sync": "同步边缘", - "sync-process-started-successfully": "同步处理开始成功!", - "type-max-length": "类型长度必须少于256个字符", - "unassign-edge-text": "确定后,边缘将被取消分配,并且客户将无法访问。", - "unassign-edge-title": "确定取消分配边缘 '{{edgeName}}' 吗?", - "unassign-edges-text": "确定后,所有选定的边缘将被取消分配,并且客户将无法访问。", - "unassign-edges-title": "确定要取消分配 {count,plural,1 {1 个边缘} other {# 个边缘} } 吗?", - "unassign-from-customer": "取消分配客户", - "unassign-from-edge": "取消分配边缘", - "widget-datasource-error": "组件只支持边缘实体数据源" - }, - "edge-event": { - "action-type-added": "增加", - "action-type-alarm-ack": "告警确认", - "action-type-alarm-clear": "告警清除", - "action-type-assigned-to-customer": "分配给客户", - "action-type-assigned-to-edge": "分配给边缘", - "action-type-attributes-deleted": "属性删除", - "action-type-attributes-updated": "属性更新", - "action-type-credentials-request": "认证请求", - "action-type-credentials-updated": "认证更新", - "action-type-deleted": "删除", - "action-type-entity-merge-request": "实体合并请求", - "action-type-post-attributes": "推送属性", - "action-type-relation-add-or-update": "关联增加或更新", - "action-type-relation-deleted": "关联删除", - "action-type-rpc-call": "RPC调用", - "action-type-timeseries-updated": "时序更新", - "action-type-unassigned-from-customer": "取消分配客户", - "action-type-unassigned-from-edge": "取消分配边缘", - "action-type-updated": "更新", - "type-admin-settings": "管理员设置", - "type-alarm": "告警", - "type-asset": "资产", - "type-customer": "客户", - "type-dashboard": "仪表板", - "type-device": "设备", - "type-device-profile": "设备概要", - "type-edge": "边缘", - "type-entity-view": "实体视图", - "type-relation": "关联", - "type-rule-chain": "规则链", - "type-rule-chain-metadata": "规则链元数据", - "type-user": "用户", - "type-widgets-bundle": "部件包", - "type-widgets-type": "部件类型" - }, - "entity-field": { - "address": "地址", - "address2": "地址二", - "city": "城市", - "country": "国家", - "created-time": "创建时间", - "email": "电子邮件", - "first-name": "名字", - "label": "标签", - "last-name": "姓氏", - "name": "名称", - "phone": "电话", - "state": "省/州", - "title": "标题", - "type": "类型", - "zip": "邮政编码" - }, - "entity-view": { - "add": "添加实体视图", - "add-alias": "添加实体视图别名", - "add-entity-view-text": "添加新实体视图", - "alias": "别名", - "alias-required": "实体视图别名必填。", - "aliases": "实体视图别名", - "any-entity-view": "任何实体视图", - "assign-entity-view-to-customer": "将实体视图分配给客户", - "assign-entity-view-to-customer-text": "请选择要分配给客户的实体视图", - "assign-entity-view-to-edge": "将实体视图分配给边缘", - "assign-entity-view-to-edge-text": "请选择要分配给边缘的实体视图", - "assign-entity-view-to-edge-title": "将实体视图分配给边缘", - "assign-entity-views": "分配实体视图", - "assign-entity-views-text": "分配 { count, plural, 1 {# 个实体视图} other {# 个实体视图} } 给客户", - "assign-new-entity-view": "分配新实体视图", - "assign-to-customer": "分配给客户", - "assign-to-customer-text": "请选择客户分配实体视图", - "assignedToCustomer": "分配给客户", - "attributes-propagation": "属性传播", - "attributes-propagation-hint": "每次保存或更新这个实体视图时,实体视图将自动从目标实体复制指定的属性。由于性能原因,目标实体属性不会在每次属性更改时传递到实体视图。您可以通过配置\"copy to view\"规则链中的规则节点,并将\"Post attributes\"和\"attributes Updated\"消息链接到新规则节点,从而启用自动传递。", - "client-attributes": "客户端属性", - "client-attributes-placeholder": "客户端属性", - "configure-alias": "配置 '{{alias}}' 别名", - "copyId": "复制实体视图ID", - "create-new-alias": "创建一个新的!", - "create-new-key": "创建一个新的!", - "created-time": "创建时间", - "date-limits": "日期限制", - "delete": "删除实体视图", - "delete-entity-view-text": "请注意:确认后实体视图及其所有相关数据将不可恢复。", - "delete-entity-view-title": "确定要删除实体视图 '{{entityViewName}}'吗?", - "delete-entity-views": "删除实体视图", - "delete-entity-views-action-title": "删除 { count, plural, 1 {# 个实体视图} other {# 个实体视图} }", - "delete-entity-views-text": "请注意:确认后,所有选定的实体视图将被删除,所有相关的数据将不可恢复。", - "delete-entity-views-title": "确定要删除 { count, plural, 1 {# 实体视图} other {# 实体视图} }吗?", - "description": "说明", - "details": "详情", - "duplicate-alias-error": "找到重复别名 '{{alias}}'。
实体视图别名必须是唯一的。", - "end-date": "结束日期", - "end-ts": "结束时间", - "enter-entity-view-type": "输入实体视图类型", - "entity-view": "实体视图", - "entity-view-alias": "实体视图别名", - "entity-view-details": "实体视图详细信息", - "entity-view-list": "实体视图列表", - "entity-view-list-empty": "没有被选中的实体视图", - "entity-view-name-filter-no-entity-view-matched": "未找到以'{{entityView}}' 开头的实体视图。", - "entity-view-name-filter-required": "实体视图名称筛选器必填。", - "entity-view-public": "实体视图是公共的", - "entity-view-required": "实体视图必填。", - "entity-view-type": "实体视图类型", - "entity-view-type-list-empty": "实体视图类型未选择。", - "entity-view-type-required": "实体视图类型必填。", - "entity-view-types": "实体视图类型", - "entity-views": "实体视图", - "events": "事件", - "idCopiedMessage": "实体视图ID已复制到剪贴板", - "make-private": "实体视图设为私有", - "make-private-entity-view-text": "确认后,实体视图及其所有数据将被私有化,无法被他人访问。", - "make-private-entity-view-title": "确定要将实体视图 '{{entityViewName}}' 设为私有吗?", - "make-public": "实体视图设为公开", - "make-public-entity-view-text": "确认后,实体视图及其所有数据将被公开并被他人访问。", - "make-public-entity-view-title": "确定要将实体视图 '{{entityViewName}}' 设为公开吗?", - "management": "实体视图管理", - "name": "名称", - "name-required": "名称必填。", - "name-starts-with": "名称前缀", - "no-alias-matching": "未找到匹配 '{{alias}}' 的别名。", - "no-aliases-found": "未找到别名。", - "no-entity-view-types-matching": "未找到匹配 '{{entitySubtype}}' 的实体视图类型。", - "no-entity-views-matching": "未找到与 '{{entity}}' 匹配的实体视图。", - "no-entity-views-text": "未找到实体视图", - "no-key-matching": "'{{key}}' 未找到。", - "no-keys-found": "未找到密钥。", - "public": "公开", - "remove-alias": "删除实体视图别名", - "search": "查找实体视图", - "select-entity-view": "选择实体视图", - "select-entity-view-type": "选择实体视图类型", - "selected-entity-views": "已选择 { count, plural, 1 {# 个实体视图} other {# 个实体视图} }", - "server-attributes": "服务端属性", - "server-attributes-placeholder": "服务端属性", - "shared-attributes": "共享属性", - "shared-attributes-placeholder": "共享属性", - "start-date": "开始日期", - "start-ts": "开始时间", - "target-entity": "目标实体", - "timeseries": "时间序列", - "timeseries-data": "时间序列数据", - "timeseries-data-hint": "配置目标实体的 Timeseries 数据键,以便实体视图可以访问这些键。此 Timeseries 数据是只读的。", - "timeseries-placeholder": "时间序列", - "unable-entity-view-device-alias-text": "实体视图别名 '{{entityViewAlias}}' 不能够被删除,因为它被下列部件所使用:
{{widgetsList}}", - "unable-entity-view-device-alias-title": "无法删除实体视图别名", - "unassign-entity-view": "未分配实体视图", - "unassign-entity-view-text": "确认后,实体视图将未分配,客户无法访问。", - "unassign-entity-view-title": "确定要取消对 '{{entityViewName}}' 实体视图的分配吗?", - "unassign-entity-view-from-edge": "未分配实体视图", - "unassign-entity-view-from-edge-text": "确认后,实体视图将未分配,边缘无法访问。", - "unassign-entity-view-from-edge-title": "确定要取消对 '{{entityViewName}}' 实体视图的分配吗?", - "unassign-entity-views": "取消分配实体视图", - "unassign-entity-views-action-title": "从客户处取消分配{count,plural,1 {# 实体视图} other {# 实体视图} }", - "unassign-entity-views-from-edge-action-title": "从边缘处取消分配{count,plural,1 {# 实体视图} other {# 实体视图} }", - "unassign-entity-views-from-edge-text": "确认后,所有选定的实体视图将被分配,边缘无法访问。", - "unassign-entity-views-from-edge-title": "确定要取消分配 { count, plural, 1 {# 个实体视图} other {# 个实体视图} } 吗?", - "unassign-entity-views-text": "确认后,所有选定的实体视图将被分配,客户无法访问。", - "unassign-entity-views-title": "确定要取消分配 { count, plural, 1 {# 个实体视图} other {# 个实体视图} } 吗?", - "unassign-from-customer": "取消分配客户", - "use-entity-view-name-filter": "使用筛选器", - "view-entity-views": "查看实体视图" - }, - "entity": { - "add-alias": "添加实体别名", - "alarm-name-starts-with": "以 '{{prefix}}' 开头的告警", - "alias": "别名", - "alias-required": "实体别名必填。", - "aliases": "实体别名", - "all-subtypes": "全部", - "any-entity": "任意实体", - "asset-name-starts-with": "以 '{{prefix}}' 开头的资产", - "columns-to-display": "要显示的列", - "configure-alias": "别名 '{{alias}}' 配置", - "create-new-alias": "创建一个新的!", - "create-new-key": "创建一个新的!", - "customer-name-starts-with": "以 '{{prefix}}' 开头的客户", - "dashboard-name-starts-with": "以 '{{prefix}}' 开头的仪表板", - "details": "实体详情", - "device-name-starts-with": "以 '{{prefix}}' 开头的设备", - "device-profile-name-starts-with": "名称以 '{{prefix}}' 开头的设备配置", - "duplicate-alias-error": "别名 '{{alias}}' 重复。
同一仪表板别名必须唯一。", - "enter-entity-type": "输入实体类型", - "entities": "实体", - "entities-count": "实体数量", - "entity": "实体", - "entity-alias": "实体别名", - "entity-label": "实体标签", - "entity-list": "实体列表", - "entity-list-empty": "没有选择实体。", - "entity-name": "实体名", - "entity-name-filter-no-entity-matched": "未找到以 '{{entity}}' 开头的实体", - "entity-name-filter-required": "实体名筛选器必填。", - "entity-type": "实体类型", - "entity-type-list": "实体类型列表", - "entity-type-list-empty": "没有选择实体类型。", - "entity-types": "实体类型", - "entity-view-name-starts-with": "以 '{{prefix}}' 开头的实体视图", - "key": "键名", - "key-name": "键名", - "list-of-alarms": "{ count, plural, 1 {1 个告警} other {# 个告警} }", - "list-of-assets": "{ count, plural, 1 {1 个资产} other {# 个资产} }", - "list-of-customers": "{ count, plural, 1 {1 个客户} other {# 个客户} }", - "list-of-dashboards": "{ count, plural, 1 {1 个仪表板} other {# 个仪表板} }", - "list-of-device-profiles": "{ count, plural, 1 {1 个设备配置} other {# 个设备配置} }", - "list-of-devices": "{ count, plural, 1 {1 个设备} other {# 个设备} }", - "list-of-entity-views": "{ count, plural, 1 {1 个实体视图} other {# 个实体视图} }", - "list-of-plugins": "{ count, plural, 1 {1 个插件} other {# 个插件} }", - "list-of-rulechains": "{ count, plural, 1 {1 个规则链} other {# 个规则链} }", - "list-of-rulenodes": "{ count, plural, 1 {1 个规则节点} other {# 个规则节点} }", - "list-of-rules": "{ count, plural, 1 {1 个规则} other {# 个规则} }", - "list-of-tenant-profiles": "{ count, plural, 1 {1 个租户配置} other {# 个租户配置} }", - "list-of-tenants": "{ count, plural, 1 {1 个租户} other {# 个租户} }", - "list-of-users": "{ count, plural, 1 {1 个用户} other {# 个用户} }", - "missing-entity-filter-error": "别名 '{{alias}}' 缺少筛选器", - "name-starts-with": "名称开始于", - "no-alias-matching": "未找到 '{{alias}}'", - "no-aliases-found": "未找到别名", - "no-data": "无数据", - "no-entities-matching": "未找到匹配 '{{entity}}' 的实体。", - "no-entities-prompt": "未找到实体", - "no-entity-types-matching": "未找到匹配 '{{entityType}}' 类型的实体。", - "no-key-matching": "未找到键名 '{{key}}'", - "no-keys-found": "未找到键名", - "plugin-name-starts-with": "以 '{{prefix}}' 开头的插件", - "remove-alias": "移除实体别名", - "rule-name-starts-with": "以 '{{prefix}}' 开头的规则", - "rulechain-name-starts-with": "规则链前缀名称 '{{prefix}}'", - "rulenode-name-starts-with": "名称以 '{{prefix}}' 开头的规则节点", - "search": "实体检索", - "select-entities": "选择实体", - "selected-entities": "已选择 { count, plural, 1 {# 个实体} other {# 个实体} }", - "tenant-name-starts-with": "以 '{{prefix}}' 开头的租户", - "tenant-profile-name-starts-with": "名称以 '{{prefix}}' 开头的租户配置", - "type": "类型", - "type-alarm": "告警", - "type-alarms": "告警", - "type-api-usage-state": "Api使用状态", - "type-asset": "资产", - "type-assets": "资产", - "type-current-customer": "当前客户", - "type-current-tenant": "当前租户", - "type-current-user": "当前用户", - "type-current-user-owner": "当前用户所有者", - "type-customer": "客户", - "type-customers": "客户", - "type-dashboard": "仪表板", - "type-dashboards": "仪表板", - "type-device": "设备", - "type-device-profile": "设备配置", - "type-device-profiles": "设备配置", - "type-devices": "设备", - "type-entity-view": "实体视图", - "type-entity-views": "实体视图", - "type-plugin": "插件", - "type-plugins": "插件", - "type-required": "实体类型必填。", - "type-rule": "规则", - "type-rulechain": "规则链", - "type-rulechains": "规则链库", - "type-rulenode": "规则节点", - "type-rulenodes": "规则节点", - "type-rules": "规则", - "type-tenant": "租户", - "type-tenant-profile": "租户简介", - "type-tenant-profiles": "租户配置", - "type-tenants": "租户", - "type-user": "用户", - "type-users": "用户", - "type-asset-profile": "资产配置", - "type-asset-profiles": "资产配置", - "unable-delete-entity-alias-text": "实体别名 '{{entityAlias}}' 被以下部件使用不能删除:
{{widgetsList}}", - "unable-delete-entity-alias-title": "无法删除实体别名", - "use-entity-name-filter": "用户筛选器", - "user-name-starts-with": "以 '{{prefix}}' 开头的用户", - "type-edge": "边缘", - "type-edges": "边缘", - "list-of-edges": "{ count, plural, 1 {1 个边缘} other {列表 # 个边缘} }", - "edge-name-starts-with": "以 '{{prefix}}' 开头的边缘" - }, - "error": { - "unable-to-connect": "无法连接到服务器!请检查您的互联网连接。", - "unhandled-error-code": "未处理的错误代码: {{errorCode}}", - "unknown-error": "未知错误" - }, - "event": { - "alarm": "告警", - "body": "整体", - "data": "数据", - "data-type": "数据类型", - "entity-id": "实体ID", - "error": "错误", - "errors-occurred": "错误发生", - "event": "事件", - "event-time": "事件时间", - "event-type": "事件类型", - "events-filter": "事件筛选器", - "failed": "失败", - "has-error": "有错误", - "message-id": "消息ID", - "message-type": "消息类型", - "messages-processed": "消息处理", - "metadata": "元数据", - "method": "方法", - "no-events-prompt": "未找到事件", - "relation-type": "关联类型", - "server": "服务器", - "status": "状态", - "success": "成功", - "type": "类型", - "type-debug-rule-chain": "调试", - "type-debug-rule-node": "调试", - "type-error": "错误", - "type-lc-event": "生命周期事件", - "type-stats": "类型统计", - "all-events": "全部", - "entity-type": "实体类型" - }, - "extension": { - "add": "添加扩展", - "add-attribute": "添加属性", - "add-attribute-request": "添加属性请求", - "add-attribute-update": "添加属性更新", - "add-broker": "添加代理服务器", - "add-config": "添加转换器配置", - "add-connect-request": "添加连接请求", - "add-converter": "添加转换器", - "add-device": "添加服务器", - "add-disconnect-request": "添加断开请求", - "add-map": "添加映射元素", - "add-server-side-rpc-request": "添加服务端RPC请求", - "add-timeseries": "添加 Timeseries", - "anonymous": "匿名", - "attr-json-key-expression": "属性键JSON表达式", - "attr-topic-key-expression": "属性键名Topic表达式", - "attribute-filter": "属性筛选器", - "attribute-key-expression": "属性键名表达式", - "attribute-requests": "属性请求", - "attribute-updates": "属性更新", - "attributes": "属性", - "basic": "Basic", - "brokers": "代理服务器组", - "ca-cert": "CA证书文件 *", - "cert": "证书文件 *", - "client-scope": "客户范围", - "configuration": "配置", - "connect-requests": "连接请求", - "converter-configurations": "转换器的配置", - "converter-id": "转换器序号", - "converter-json": "Json", - "converter-json-parse": "无法解析转换JSON。", - "converter-json-required": "Converter JSON 必填。", - "converter-type": "转换类型", - "converters": "转换器", - "credentials": "证书", - "custom": "定制", - "delete": "删除扩展", - "delete-extension-text": "请注意:确认后,扩展和所有相关数据将不可恢复。", - "delete-extension-title": "确定要删除扩展 '{{extensionId}}'吗?", - "delete-extensions-text": "请注意:确认后,所有选定的扩展将被删除,所有相关数据将不可恢复。", - "delete-extensions-title": "确定要删除 { count, plural, 1 {# 个扩展} other {# 个扩展} }吗?", - "device-name-expression": "设备名称表达式", - "device-name-filter": "设备名称筛选器", - "device-type-expression": "设备类型表达式", - "disconnect-requests": "断开请求", - "drop-file": "删除文件或单击以选择要上载的文件。", - "edit": "编辑扩展", - "export-extension": "导出扩展", - "export-extensions-configuration": "导出扩展配置", - "extension-id": "扩展ID", - "extension-type": "扩展类型", - "extensions": "扩展", - "field-required": "必填字段", - "file": "扩展文件", - "filter-expression": "筛选条件表达式", - "host": "主机", - "id": "ID", - "import-extension": "导入扩展", - "import-extensions": "导入扩展", - "import-extensions-configuration": "导入扩展配置", - "invalid-file-error": "无效的扩展文件", - "json-name-expression": "设备名称JSON表达式", - "json-parse": "无法解析转换器JSON。", - "json-required": "Transformer JSON 必填。", - "json-type-expression": "设备类型JSON表达式", - "key": "键名", - "mapping": "映射", - "method-filter": "方法筛选器", - "modbus-add-server": "添加 server/slave", - "modbus-add-server-prompt": "请添加 server/slave", - "modbus-attributes-poll-period": "轮询属性周期 (毫秒)", - "modbus-baudrate": "波特率", - "modbus-byte-order": "字节顺序", - "modbus-databits": "数据位", - "modbus-databits-range": "数据位应该在7到8的范围内。", - "modbus-device-name": "设备名称", - "modbus-encoding": "编码", - "modbus-function": "函数", - "modbus-parity": "奇偶性", - "modbus-poll-period": "轮询周期 (毫秒)", - "modbus-poll-period-range": "轮询周期应为正值。", - "modbus-port-name": "串口名称", - "modbus-register-address": "寄存器地址", - "modbus-register-address-range": "寄存器地址应该在0到65535的范围内。", - "modbus-register-bit-index": "位索引", - "modbus-register-bit-index-range": "位索引应该在0到15的范围内。", - "modbus-register-count": "寄存器计数", - "modbus-register-count-range": "寄存器计数应该是一个正值。", - "modbus-rtu-over-tcp": "RTU over TCP", - "modbus-server": "Servers/slaves", - "modbus-stopbits": "停止位", - "modbus-stopbits-range": "停止位应该在1到2的范围内。", - "modbus-tag": "标签", - "modbus-tcp-reconnect": "自动重新连接", - "modbus-timeseries-poll-period": "Timeseries 轮询周期 (毫秒)", - "modbus-transport": "Transport", - "modbus-unit-id": "单位编号", - "modbus-unit-id-range": "单位ID应该在1到247的范围内", - "no-file": "没有选择文件。", - "opc-add-server": "添加服务器", - "opc-add-server-prompt": "请添加服务器", - "opc-application-name": "应用名称", - "opc-application-uri": "应用URI", - "opc-device-name-pattern": "设备名称模式", - "opc-device-node-pattern": "设备节点模式", - "opc-identity": "身份", - "opc-keystore": "密钥库", - "opc-keystore-alias": "别名", - "opc-keystore-key-password": "密钥密码", - "opc-keystore-location": "位置 *", - "opc-keystore-password": "密码", - "opc-keystore-type": "类型", - "opc-scan-period-in-seconds": "秒级扫描周期", - "opc-security": "安全性", - "opc-server": "服务器组", - "opc-type": "类型", - "password": "密码", - "pem": "PEM", - "port": "端口", - "port-range": "端口应该在1到65535的范围内。", - "private-key": "私钥文件 *", - "request-id-expression": "请求ID表达式", - "request-id-json-expression": "请求ID JSON表达式", - "request-id-topic-expression": "请求ID主题表达式", - "request-topic-expression": "请求主题表达式", - "response-timeout": "响应超时(毫秒)", - "response-topic-expression": "响应主题表达式", - "retry-interval": "重试间隔(毫秒)", - "selected-extensions": "已选择 { count, plural, 1 {# 个扩展} other {# 个扩展} }", - "server-side-rpc": "服务端RPC", - "ssl": "SSL", - "sync": { - "last-sync-time": "最后同步时间", - "not-available": "无法使用", - "not-sync": "不同步", - "status": "状态", - "sync": "同步" - }, - "timeout": "超时时间(毫秒)", - "timeseries": "Timeseries", - "to-double": "加倍", - "token": "安全令牌", - "topic": "主题", - "topic-expression": "主题表达", - "topic-filter": "Topic筛选器", - "topic-name-expression": "设备名称主题表达式", - "topic-type-expression": "设备类型主题表达式", - "transformer": "转换器", - "transformer-json": "JSON *", - "type": "类型", - "unique-id-required": "当前扩展ID已经存在。", - "username": "用户名", - "value": "价值", - "value-expression": "值表达式" - }, - "filter": { - "add": "添加筛选器", - "add-complex": "添加复合", - "add-complex-filter": "添加复合筛选器", - "add-filter": "添加筛选器", - "add-filter-prompt": "请添加筛选器", - "add-key-filter": "添加键名筛选器", - "autogenerated-label": "自动生成标签", - "complex-filter": "复合筛选器", - "create-new-filter": "请新增!", - "current-customer": "当前客户", - "current-device": "当前设备", - "current-tenant": "当前租户", - "current-user": "当前用户", - "date": "日期", - "default-value": "默认值", - "display-label": "要显示的标签", - "duplicate-filter": "同名筛选器已存在。", - "duplicate-filter-error": "找到重复的筛选器 '{{filter}}'。
筛选器在仪表板中必须是唯一的。", - "dynamic-source-type": "动态源类型", - "edit": "编辑筛选器", - "edit-complex-filter": "编辑复合筛选器", - "edit-filter-user-params": "编辑筛选器谓词用户参数", - "edit-key-filter": "编辑键名筛选器", - "editable": "可编辑", - "filter": "筛选器", - "filter-required": "筛选器必填。", - "filter-user-params": "过滤谓词用户参数", - "filters": "筛选器", - "ignore-case": "忽略大小写", - "key-filter": "键名筛选器", - "key-filters": "键名筛选器", - "key-name": "键名", - "key-name-required": "键名必填。", - "key-type": { - "attribute": "属性", - "constant": "常量", - "entity-field": "实体", - "key-type": "键类型", - "timeseries": "Timeseries" - }, - "key-value-type-change-message": "如果您确认新的值类型,所有输入的键过滤器将被删除。", - "key-value-type-change-title": "确定要更改键值类型吗?", - "missing-key-filters-error": "筛选器 '{{filter}}' 的键名筛选条件缺失。", - "name": "筛选器名称", - "name-required": "筛选器名称必填。", - "no-dynamic-value": "无动态值", - "no-filter-matching": "未找到 '{{filter}}' 。", - "no-filter-text": "未指定筛选器", - "no-filters": "未配置筛选器", - "no-filters-found": "未找到筛选器。", - "no-key-filters": "未配置键名筛选器", - "operation": { - "and": "和", - "contains": "包含", - "ends-with": "结束于", - "equal": "等于", - "greater": "大于", - "greater-or-equal": "大于或等于", - "less": "小于", - "less-or-equal": "小于或等于", - "not-contains": "不包含", - "not-equal": "不等于", - "operation": "操作", - "or": "或", - "starts-with": "开始于" - }, - "order-priority": "字段顺序优先级", - "preview": "筛选器预览", - "remove-filter": "删除筛选器", - "remove-key-filter": "删除键名筛选器", - "source-attribute": "源属性", - "switch-to-default-value": "切换到默认值", - "switch-to-dynamic-value": "切换到动态值", - "time": "时间", - "unable-delete-filter-text": "无法删除筛选器 '{{filter}}' ,因为它由以下小部件使用:
{{widgetsList}}", - "unable-delete-filter-title": "无法删除筛选器", - "user-parameters": "用户参数", - "value": "值", - "value-type-required": "键值类型是必需的。", - "value-type": { - "boolean": "布尔值", - "date-time": "日期时间", - "numeric": "数字", - "string": "字符串", - "value-type": "值类型" - } - }, - "fullscreen": { - "exit": "退出全屏", - "expand": "展开到全屏", - "fullscreen": "全屏", - "toggle": "切换全屏模式" - }, - "function": { - "function": "函数" - }, - "gateway": { - "add-entry": "添加配置", - "connector-add": "添加新连接器", - "connector-enabled": "启用连接器", - "connector-name": "连接器名称", - "connector-name-required": "Connector 名称必填。", - "connector-type": "连接器类型", - "connector-type-required": "Connector 类型必填。", - "connectors": "连接器配置", - "create-new-gateway": "创建新网关", - "create-new-gateway-text": "确定要创建名为 '{{gatewayName}}' 的新网关?", - "delete": "删除配置", - "download-tip": "下载配置", - "gateway": "网关", - "gateway-exists": "同名设备已存在。", - "gateway-name": "网关名称", - "gateway-name-required": "网关名称必填。", - "gateway-saved": "已成功保存网关配置。", - "json-parse": "无效的JSON。", - "json-required": "字段不能为空。", - "no-connectors": "无连接器", - "no-data": "没有配置", - "no-gateway-found": "未找到网关。", - "no-gateway-matching": "未找到 '{{item}}' 。", - "path-logs": "日志文件的路径", - "path-logs-required": "路径是必需的。", - "remote": "远程配置", - "remote-logging-level": "日志记录级别", - "remove-entry": "删除配置", - "save-tip": "保存配置", - "security-type": "安全类型", - "security-types": { - "access-token": "访问令牌", - "tls": "TLS" - }, - "storage": "存储", - "storage-max-file-records": "文件中的最大记录数", - "storage-max-files": "最大文件数", - "storage-max-files-min": "最小值为1。", - "storage-max-files-pattern": "数字无效。", - "storage-max-files-required": "数字是必需的。", - "storage-max-records": "存储中的最大记录数", - "storage-max-records-min": "最小记录数为1。", - "storage-max-records-pattern": "数字无效。", - "storage-max-records-required": "最大记录项必填。", - "storage-pack-size": "最大事件包大小", - "storage-pack-size-min": "最小值为1。", - "storage-pack-size-pattern": "数字无效。", - "storage-pack-size-required": "最大事件包大小必填。", - "storage-path": "存储路径", - "storage-path-required": "存储路径必填。", - "storage-type": "存储类型", - "storage-types": { - "file-storage": "文件存储", - "memory-storage": "内存存储" - }, - "thingsboard": "ThingsBoard", - "thingsboard-host": "ThingsBoard主机", - "thingsboard-host-required": "主机必填。", - "thingsboard-port": "ThingsBoard端口", - "thingsboard-port-max": "最大端口号为65535。", - "thingsboard-port-min": "最小端口号为1。", - "thingsboard-port-pattern": "端口无效。", - "thingsboard-port-required": "端口必填。", - "tidy": "整洁", - "tidy-tip": "整理配置JSON", - "title-connectors-json": "连接器 {{typeName}} 配置", - "tls-path-ca-certificate": "网关上CA证书的路径", - "tls-path-client-certificate": "网关上客户端证书的路径", - "tls-path-private-key": "网关上私钥的路径", - "toggle-fullscreen": "切换全屏", - "transformer-json-config": "配置JSON*", - "update-config": "添加/更新配置JSON" - }, - "grid": { - "add-item-text": "添加新项目", - "delete-item": "删除项目", - "delete-item-text": "请注意,确认后,项目及其所有相关数据将不可恢复。", - "delete-item-title": "确定要删除此项吗?", - "delete-items": "删除项目", - "delete-items-action-title": "删除 { count, plural, 1 {# 个元素} other {# 个元素} }", - "delete-items-text": "请注意,确认后所有选择的项目将被删除,所有相关数据将不可恢复。", - "delete-items-title": "确定删除 { count, plural, 1 {# 项} other {# 项} }吗?", - "item-details": "项目详细信息", - "no-items-text": "未找到项目", - "scroll-to-top": "滚动到顶部" - }, - "help": { - "goto-help-page": "转到帮助页面" - }, - "home": { - "avatar": "头像", - "home": "首页", - "logout": "注销", - "menu": "菜单", - "open-user-menu": "打开用户菜单", - "profile": "属性" - }, - "icon": { - "icon": "图标", - "material-icons": "素材图标", - "select-icon": "选择图标", - "show-all": "显示所有图标" - }, - "phone-input": { - "phone-input-label": "手机号码", - "phone-input-required": "手机号码必填", - "phone-input-validation": "手机号码无效或不存在", - "phone-input-pattern": "无效的手机号码。应为E.164格式,例如:{{phoneNumber}}", - "phone-input-hint": "E.164格式手机号码,例如:{{phoneNumber}}" - }, - "import": { - "column-example": "示例值数据", - "column-key": "属性/遥测键", - "column-title": "标题", - "column-type": { - "access-token": "访问令牌", - "client-attribute": "客户端属性", - "column-type": "列类型", - "description": "说明", - "entity-field": "实体字段", - "isgateway": "是否网关", - "label": "标签", - "name": "名称", - "routing-key": "边缘键", - "secret": "边缘密钥", - "server-attribute": "服务器属性", - "shared-attribute": "共享属性", - "timeseries": "Timeseries", - "type": "类型" - }, - "column-value": "数值", - "csv-delimiter": "CSV分隔符", - "csv-first-line-header": "第一行包含列名", - "csv-update-data": "更新属性/遥测", - "drop-file": "拖动一个JSON文件或者单击以选择要上传的文件。", - "drop-file-csv": "拖动一个CSV文件或单击以选择要上载的文件。", - "import-csv-invalid-format-error": "文件格式无效。行: '{{line}}'", - "import-csv-number-columns-error": "一个文件至少应该包含两列", - "message": { - "create-entities": "{{count}} 个新实体已成功创建。", - "error-entities": "创建 {{count}} 个实体时出错。", - "update-entities": "{{count}} 个实体已成功更新。" - }, - "no-file": "没有选择文件", - "stepper-text": { - "column-type": "选择列类型", - "configuration": "导入配置", - "creat-entities": "创建新实体", - "done": "完成", - "select-file": "选择一个文件" - } - }, - "item": { - "selected": "选择" - }, - "js-func": { - "mini": "迷你", - "no-return-error": "函数必须返回值!", - "return-type-mismatch": "函数必须返回 '{{type}}' 类型的值!", - "tidy": "整洁" - }, - "key-val": { - "add-entry": "增加条目", - "key": "键名", - "no-data": "没有条目", - "remove-entry": "删除条目", - "value": "数值" - }, - "paginator": { - "items-per-page-label": "每页条数", - "next-page-label": "下一页", - "previous-page-label": "上一页", - "first-page-label": "首页", - "last-page-label": "尾页", - "range_page_label_1": "共计 {{length}} 条", - "range_page_label_2": "从第 {{startIndex}} 条到第 {{endIndex}} 条, 共计 {{length}} 条" - }, - "language": { - "language": "语言" - }, - "layout": { - "color": "颜色", - "layout": "布局", - "main": "主体", - "manage": "布局管理", - "right": "右侧", - "select": "选择目标布局", - "settings": "布局设置" - }, - "legend": { - "avg": "平均值", - "comparison-time-ago": { - "days": "(一天前)", - "months": "(一个月前)", - "weeks": "(一周前)", - "years": "(一年前)" - }, - "direction": "图例方向", - "max": "最大值", - "min": "最小值", - "position": "图例位置", - "settings": "图例设置", - "show-avg": "显示平均值", - "show-max": "显示最大值", - "show-min": "显示最小值", - "show-total": "显示总数", - "sort-legend": "在图例中排序数据键", - "total": "总数" - }, - "login": { - "create-password": "创建密码", - "email": "电子邮件", - "error": "登录错误", - "expired-password-reset-message": "您的凭据已过期!请创建新密码。", - "forgot-password": "忘记密码?", - "login": "登录", - "login-with": "使用 {{name}} 登录", - "new-password": "新密码", - "new-password-again": "再次输入新密码", - "or": "或", - "password-again": "再次输入密码", - "password-link-sent-message": "密码重置链接已成功发送!", - "password-reset": "密码重置", - "passwords-mismatch-error": "输入的密码必须相同!", - "remember-me": "记住我", - "request-password-reset": "请求密码重置", - "reset-password": "重置密码", - "sign-in": "登录 ", - "username": "用户名(电子邮件)", - "verify-your-identity": "身份验证", - "select-way-to-verify": "选择验证方式", - "resend-code": "重发验证码", - "resend-code-wait": "{ time, plural, 1 {1 秒} other {# 秒} }后重发验证码", - "try-another-way": "尝试其他方法", - "totp-auth-description": "请从验证APP中查看验证码。", - "totp-auth-placeholder": "验证码", - "sms-auth-description": "验证码已发送到手机号码 {{contact}}。", - "sms-auth-placeholder": "SMS 验证码", - "email-auth-description": "验证码已发送到电子邮箱地址 {{contact}}。", - "email-auth-placeholder": "电子邮件验证码", - "backup-code-auth-description": "请输入一个备份验证码。", - "backup-code-auth-placeholder": "备份验证码" - }, - "ota-update": { - "add": "添加包", - "assign-firmware": "分配的固件", - "assign-firmware-required": "分配的固件必填", - "assign-software": "分配的软件", - "assign-software-required": "分配的软件必填", - "auto-generate-checksum": "自动生成校验和", - "checksum": "校验和", - "checksum-hint": "如果校验和为空,会自动生成", - "checksum-algorithm": "校验和算法", - "checksum-copied-message": "包校验和已复制到剪贴板", - "change-firmware": "固件的更改可能会导致 { count, plural, 1 {1 个设备} other {# 个设备} } 的更新。", - "change-software": "软件的更改可能会导致 { count, plural, 1 {1 个设备} other {# 个设备} } 的更新。", - "chose-compatible-device-profile": "上传的包仅适用于具有所选配置文件的设备。", - "chose-firmware-distributed-device": "选择将分发到设备的固件", - "chose-software-distributed-device": "选择将分发到设备的软件", - "content-type": "内容类型", - "copy-checksum": "复制校验和", - "copy-direct-url": "复制直接URL", - "copyId": "复制包ID", - "copied": "已复制!", - "delete": "删除包", - "delete-ota-update-text": "请注意:确认后,OTA升级将不可恢复。", - "delete-ota-update-title": "确定要删除 '{{title}}' OTA升级吗?", - "delete-ota-updates-text": "请注意:确认后,所有选中的OTA升级将被删除。", - "delete-ota-updates-title": "确定要删除 { count, plural, 1 {1 OTA升级} other {# OTA升级} } 吗?", - "description": "说明", - "direct-url": "直接URL", - "direct-url-copied-message": "包直接URL已复制到剪贴板", - "direct-url-required": "直接URL必填", - "download": "下载包", - "drop-file": "拖放打包文件或点击选择要上传的文件。", - "file-name": "文件名", - "file-size": "文件大小", - "file-size-bytes": "文件大小(以字节为单位)", - "idCopiedMessage": "包ID已被复制到剪贴板", - "no-firmware-matching": "未找到与'{{entity}}'匹配的兼容固件OTA升级包。", - "no-firmware-text": "没有提供兼容的固件OTA升级包。", - "no-packages-text": "未找到包", - "no-software-matching": "未找到匹配 '{{entity}}' 的兼容软件OTA升级包。", - "no-software-text": "没有提供兼容的软件OTA升级包。", - "ota-update": "OTA 升级", - "ota-update-details": "OTA 升级详情", - "ota-updates": "OTA 升级", - "package-type": "包类型", - "packages-repository": "包仓库", - "search": "搜索包", - "selected-package": "{ count, plural, 1 {1 个包} other {# 个包} } 选中", - "title": "标题", - "title-required": "标题必填。", - "types": { - "firmware": "固件", - "software": "软件" - }, - "upload-binary-file": "上传二进制文件", - "use-external-url": "使用外部URL", - "version": "版本", - "version-required": "版本必填。", - "version-tag": "版本标签", - "version-tag-hint": "自定义标签应与您设备报告的软件包版本相匹配。", - "warning-after-save-no-edit": "上传包后,您将无法修改标题、版本、设备配置文件和包类型。" - }, - "position": { - "bottom": "底部", - "left": "左侧", - "right": "右侧", - "top": "顶部" - }, - "profile": { - "change-password": "更改密码", - "current-password": "当前密码", - "last-login-time": "最后登录", - "profile": "属性", - "jwt-token": "JWT 令牌", - "copy-jwt-token": "复制 JWT 令牌", - "token-valid-till": "令牌有效期至", - "tokenCopiedSuccessMessage": "JWT 令牌已复制到剪贴板", - "tokenCopiedWarnMessage": "JWT 令牌已过期!请刷新页面。" - }, - "profiles": { - "profiles": "配置" - }, - "security": { - "security": "安全", - "2fa": { - "2fa": "双因素身份验证", - "2fa-description": "双因素身份验证可保护您的帐户免受未经授权的访问。在登录时必须输入安全验证码。", - "authenticate-with": "可以使用以下身份验证:", - "disable-2fa-provider-text": "禁用 {{ name }} 会降低帐户的安全性", - "disable-2fa-provider-title": "确定要禁用 {{ name }} 吗?", - "get-new-code": "获取新验证码", - "main-2fa-method": "用作主要的双因素身份验证方法", - "dialog": { - "activation-step-description-email": "下次登录时,系统将提示您输入电子邮件中的验证码。", - "activation-step-description-sms": "下次登录时,系统将提示您输入短信中的验证码。", - "activation-step-description-totp": "下次登录时,您需要提供一个双因素身份验证码。", - "activation-step-label": "激活", - "backup-code-description": "打印出验证码,以便在您需要时使用它们登录帐户。每个备份验证码可以使用一次。", - "backup-code-warn": "离开此页后,这些代码将无法再次显示。使用以下选项安全存放。", - "download-txt": "下载(txt)", - "email-step-description": "输入用作身份验证的电子邮件", - "email-step-label": "电子邮件", - "enable-email-title": "启用电子邮件验证", - "enable-sms-title": "启用SMS验证", - "enable-totp-title": "启用验证APP", - "enter-verification-code": "输入6位验证码", - "get-backup-code-title": "获取备份验证码", - "next": "下一步", - "scan-qr-code": "使用验证APP扫描二维码", - "send-code": "发送验证码", - "sms-step-description": "输入用作身份验证的手机号码", - "sms-step-label": "手机号码", - "success": "操作成功!", - "totp-step-description-install": "可以安装像Google Authenticator、Authy或Duo这样的应用程序。", - "totp-step-description-open": "在手机上打开验证APP。", - "totp-step-label": "获取APP", - "verification-code": "6位验证码", - "verification-code-invalid": "验证码格式无效", - "verification-code-incorrect": "验证码不正确", - "verification-code-many-request": "请求过多,请检查验证码", - "verification-step-description": "输入发送到 {{ address }} 的6位代码", - "verification-step-label": "验证" - }, - "provider": { - "email": "电子邮件", - "email-description": "使用您电子邮件中的验证码进行身份验证。", - "email-hint": "身份验证码通过电子邮件发送到 {{ info }}", + "access": { + "unauthorized": "未授权", + "unauthorized-access": "未授权访问", + "unauthorized-access-text": "你需要登陆才能访问这个资源!", + "access-forbidden": "禁止访问", + "access-forbidden-text": "你没有访问此位置的权限
如果你仍希望访问此位置,请尝试使用其他用户登录。", + "refresh-token-expired": "会话已过期", + "refresh-token-failed": "无法刷新会话", + "permission-denied": "权限被拒绝", + "permission-denied-text": "你没有执行此操作的权限!" + }, + "action": { + "activate": "激活", + "suspend": "暂停", + "save": "保存", + "saveAs": "另存为", + "cancel": "取消", + "ok": "确定", + "delete": "删除", + "add": "添加", + "yes": "是", + "no": "否", + "update": "更新", + "remove": "移除", + "select": "选择", + "search": "查询", + "clear-search": "清除查询", + "assign": "分配", + "unassign": "取消分配", + "share": "分享", + "make-private": "私有", + "apply": "应用", + "apply-changes": "应用更改", + "edit-mode": "编辑模式", + "enter-edit-mode": "进入编辑模式", + "decline-changes": "撤销更改", + "close": "关闭", + "back": "后退", + "run": "运行", + "sign-in": "登录!", + "edit": "编辑", + "view": "查看", + "create": "创建", + "drag": "拖拽", + "refresh": "刷新", + "undo": "撤销", + "copy": "复制", + "paste": "粘贴", + "copy-reference": "复制引用", + "paste-reference": "粘贴引用", + "import": "导入", + "export": "导出", + "share-via": "通过{{provider}}分享", + "continue": "继续", + "discard-changes": "放弃更改", + "download": "下载", + "next-with-label": "下一个:{{label}}", + "read-more": "阅读更多", + "hide": "隐藏", + "done": "完毕", + "print": "打印", + "restore": "恢复", + "confirm": "确认" + }, + "aggregation": { + "aggregation": "聚合", + "function": "函数", + "limit": "限制", + "group-interval": "分组间隔", + "min": "最小值", + "max": "最大值", + "avg": "平均值", + "sum": "求和", + "count": "计数", + "none": "空" + }, + "admin": { + "general": "基础设置", + "general-settings": "基础设置", + "home-settings": "首页设置", + "outgoing-mail": "发送邮件", + "outgoing-mail-settings": "发送邮件设置", + "system-settings": "系统设置", + "test-mail-sent": "测试邮件发送成功!", + "base-url": "基本URL", + "base-url-required": "基本URL必填。", + "prohibit-different-url": "禁止从客户端请求头中使用主机名", + "prohibit-different-url-hint": "应为生产环境启用此设置。禁用时可能会导致安全问题", + "mail-from": "邮件来自", + "mail-from-required": "邮件发件人必填。", + "smtp-protocol": "SMTP协议", + "smtp-host": "SMTP主机", + "smtp-host-required": "SMTP主机必填。", + "smtp-port": "SMTP端口", + "smtp-port-required": "你必须提供一个smtp端口。", + "smtp-port-invalid": "这看起来不是有效的smtp端口。", + "timeout-msec": "超时(毫秒)", + "timeout-required": "超时必填。", + "timeout-invalid": "超时值无效。", + "enable-tls": "启用TLS", + "tls-version": "TLS版本", + "enable-proxy": "启用代理", + "proxy-host": "代理主机", + "proxy-host-required": "代理主机必填。", + "proxy-port": "代理端口", + "proxy-port-required": "代理端口必填。", + "proxy-port-range": "代理端口应在1到65535之间。", + "proxy-user": "代理用户", + "proxy-password": "代理密码", + "change-password": "更改密码", + "send-test-mail": "发送测试邮件", + "sms-provider": "SMS服务商", + "sms-provider-settings": "SMS服务商设置", + "sms-provider-type": "SMS服务商类型", + "sms-provider-type-required": "SMS服务商类型必填。", + "sms-provider-type-aws-sns": "亚马逊社交网站", + "sms-provider-type-twilio": "Twilio", + "sms-provider-type-smpp": "SMPP", + "aws-access-key-id": "AWS访问密钥ID", + "aws-access-key-id-required": "需要访问AWS密钥ID", + "aws-secret-access-key": "AWS秘密访问密钥", + "aws-secret-access-key-required": "AWS访问密钥必填", + "aws-region": "AWS地区", + "aws-region-required": "AWS地区必填", + "number-from": "发送方电话号码", + "number-from-required": "发送方电话号码必填。", + "number-to": "电话号码至", + "number-to-required": "电话号码必填。", + "phone-number-hint": "E.164格式的电话号码,例如+19995550123", + "phone-number-hint-twilio": "E.164 格式的电话号码/电话号码的SID/消息服务SID,例如。 +19995550123/PNXXX/MGXXX", + "phone-number-pattern": "电话号码无效。应为E.164格式,例如+19995550123。", + "phone-number-pattern-twilio": "无效的电话号码。应该是E.164格式/电话号码的SID/消息服务SID,例如:+19995550123/PNXXX/MGXXX。", + "sms-message": "短信", + "sms-message-required": "短信内容必填。", + "sms-message-max-length": "短信长度不能超过1600个字符", + "twilio-account-sid": "Twilio帐户SID", + "twilio-account-sid-required": "Twilio 帐户的SID必填", + "twilio-account-token": "Twilio帐户令牌", + "twilio-account-token-required": "Twilio帐户令牌必填", + "send-test-sms": "发送测试短信", + "test-sms-sent": "测试短信发送成功!", + "security-settings": "安全设置", + "password-policy": "密码策略", + "minimum-password-length": "最小密码长度", + "minimum-password-length-required": "最小密码长度必填", + "minimum-password-length-range": "最小密码长度应在5到50之间", + "minimum-uppercase-letters": "最小大写字母数", + "minimum-uppercase-letters-range": "大写字母的最小数目不能为负数", + "minimum-lowercase-letters": "最小小写字母数", + "minimum-lowercase-letters-range": "小写字母的最小数目不能为负数", + "minimum-digits": "最小位数", + "minimum-digits-range": "最小位数不能为负数", + "minimum-special-characters": "最小特殊字符数", + "minimum-special-characters-range": "特殊字符的最小数目不能为负数", + "password-expiration-period-days": "密码有效期(天)", + "password-expiration-period-days-range": "密码过期期限(天)不能为负", + "password-reuse-frequency-days": "密码重用频率(天)", + "password-reuse-frequency-days-range": "天内密码重用频率不能为负", + "allow-whitespace": "允许空格", + "general-policy": "基本策略", + "max-failed-login-attempts": "登录失败之前的最大登录尝试次数", + "minimum-max-failed-login-attempts-range": "登录失败次数不能为负数", + "user-lockout-notification-email": "如果用户帐户锁定,请发送通知到电子邮件", + "domain-name": "域名", + "domain-name-unique": "域名必须是唯一的。", + "domain-name-max-length": "域名长度不能大于256", + "error-verification-url": "域名不应包含符号“/”和“:”。例:thingsboard.io", + "oauth2": { + "access-token-uri": "访问令牌URI", + "access-token-uri-required": "访问令牌URI必填。", + "activate-user": "激活用户", + "add-domain": "添加域名", + "delete-domain": "删除域名", + "add-provider": "添加提供程序", + "delete-provider": "删除提供程序", + "allow-user-creation": "允许用户创建", + "always-fullscreen": "始终全屏", + "authorization-uri": "授权URI", + "authorization-uri-required": "授权URI必填。", + "client-authentication-method": "客户端身份验证方法", + "client-id": "客户端ID", + "client-id-required": "客户端ID必填。", + "client-id-max-length": "客户端ID长度不能大于256", + "client-secret": "客户端密钥", + "client-secret-required": "客户端密钥必填。", + "client-secret-max-length": "客户端密钥长度不能大于2049", + "custom-setting": "自定义设置", + "customer-name-pattern": "客户名称模式", + "customer-name-pattern-max-length": "客户名称模式长度不能大于256", + "default-dashboard-name": "默认仪表板名称", + "default-dashboard-name-max-length": "默认仪表板名称长度不能大于256", + "delete-domain-text": "请注意:确认删除后域名和所提供程序数据将不可用。", + "delete-domain-title": "确实要删除域名'{{domainName}}'的设置吗?", + "delete-registration-text": "请注意:确认删除后提供程序数据将不可用。", + "delete-registration-title": "确实要删除提供程序'{{name}}'吗?", + "email-attribute-key": "电子邮件属性键", + "email-attribute-key-required": "电子邮件属性键必填。", + "email-attribute-key-max-length": "电子邮件属性键长度不能大于32", + "first-name-attribute-key": "名字属性键", + "first-name-attribute-key-max-length": "名字属性键长度不能大于32", + "general": "基本设置", + "jwk-set-uri": "JSON Web Key URI", + "last-name-attribute-key": "姓氏属性键", + "last-name-attribute-key-max-length": "姓氏属性键长度不能大于32", + "login-button-icon": "登录按钮图标", + "login-button-label": "提供程序标签", + "login-button-label-placeholder": "使用$(Provider label)登录", + "login-button-label-required": "标签必填。", + "login-provider": "提供程序登录", + "mapper": "属性映射", + "new-domain": "新建域名", + "oauth2": "OAuth2", + "password-max-length": "密码长度不能大于256", + "redirect-uri-template": "重定向URI模板", + "copy-redirect-uri": "复制重定向URI", + "registration-id": "注册ID", + "registration-id-required": "注册ID必填。", + "registration-id-unique": "系统的注册ID必须是唯一的。", + "scope": "范围", + "scope-required": "范围必填。", + "tenant-name-pattern": "租户名称模式", + "tenant-name-pattern-required": "租户名称模式必填。", + "tenant-name-pattern-max-length": "户名称模式长度不能大于256", + "tenant-name-strategy": "租户名称策略", + "type": "映射器类型", + "uri-pattern-error": "无效的URI格式。", + "url": "URL地址", + "url-pattern": "无效的URL格式。", + "url-required": "URL地址必填。", + "url-max-length": "URL地址长度不能大于256", + "user-info-uri": "用户信息URI", + "user-info-uri-required": "用户信息URI必填。", + "username-max-length": "用户名称长度不能大于256", + "user-name-attribute-name": "用户名属性键", + "user-name-attribute-name-required": "用户名属性密钥必填", + "protocol": "协议", + "domain-schema-http": "HTTP", + "domain-schema-https": "HTTPS", + "domain-schema-mixed": "HTTP+HTTPS", + "enable": "启用OAuth2设置", + "domains": "域名", + "mobile-apps": "移动端", + "no-mobile-apps": "无移动端应用配置", + "mobile-package": "应用程包", + "mobile-package-placeholder": "例如: my.example.app", + "mobile-package-hint": "Android:应用程序ID,iOS:产品标识符", + "mobile-package-unique": "应用程序包必须是唯一的。", + "mobile-app-secret": "应用程序密钥", + "invalid-mobile-app-secret": "应用程序密钥必须包含字母,数字,字符并且长度必须在16到2048个字符。", + "copy-mobile-app-secret": "复制应用程序密钥", + "add-mobile-app": "添加应用程序", + "delete-mobile-app": "删除应用程序", + "providers": "提供程序", + "platform-web": "Web", + "platform-android": "Android", + "platform-ios": "iOS", + "all-platforms": "所有平台", + "allowed-platforms": "允许平台" + }, + "smpp-provider": { + "smpp-version": "SMPP版本", + "smpp-host": "SMPP主机", + "smpp-host-required": "SMPP主机必填", + "smpp-port": "SMPP端口", + "smpp-port-required": "SMPP端口必填", + "system-id": "系统ID", + "system-id-required": "系统ID必填", + "password": "密码", + "password-required": "密码必填", + "type-settings": "类型设置", + "source-settings": "源设置", + "destination-settings": "目的地", + "additional-settings": "其他设置", + "system-type": "系统类型", + "bind-type": "绑定类型", + "service-type": "服务类型", + "source-address": "源地址", + "source-ton": "源TON", + "source-npi": "源NPI", + "destination-ton": "目的地TON号码", + "destination-npi": "目的地NPI号码", + "address-range": "地址范围", + "coding-scheme": "编码方案", + "bind-type-tx": "发送者", + "bind-type-rx": "接收者", + "bind-type-trx": "收发器", + "ton-unknown": "未知", + "ton-international": "International", + "ton-national": "National", + "ton-network-specific": "Network Specific", + "ton-subscriber-number": "Subscriber Number", + "ton-alphanumeric": "Alphanumeric", + "ton-abbreviated": "Abbreviated", + "npi-unknown": "0 - Unknown", + "npi-isdn": "1 - ISDN/telephone numbering plan (E163/E164)", + "npi-data-numbering-plan": "3 - Data numbering plan (X.121)", + "npi-telex-numbering-plan": "4 - Telex numbering plan (F.69)", + "npi-land-mobile": "6 - Land Mobile (E.212)", + "npi-national-numbering-plan": "8 - National numbering plan", + "npi-private-numbering-plan": "9 - Private numbering plan", + "npi-ermes-numbering-plan": "10 - ERMES numbering plan (ETSI DE/PS 3 01-3)", + "npi-internet": "13 - Internet (IP)", + "npi-wap-client-id": "18 - WAP Client Id (to be defined by WAP Forum)", + "scheme-smsc": "0 - SMSC Default Alphabet (ASCII for short and long code and to GSM for toll-free)", + "scheme-ia5": "1 - IA5 (ASCII for short and long code, Latin 9 for toll-free (ISO-8859-9))", + "scheme-octet-unspecified-2": "2 - Octet Unspecified (8-bit binary)", + "scheme-latin-1": "3 - Latin 1 (ISO-8859-1)", + "scheme-octet-unspecified-4": "4 - Octet Unspecified (8-bit binary)", + "scheme-jis": "5 - JIS (X 0208-1990)", + "scheme-cyrillic": "6 - Cyrillic (ISO-8859-5)", + "scheme-latin-hebrew": "7 - Latin/Hebrew (ISO-8859-8)", + "scheme-ucs-utf": "8 - UCS2/UTF-16 (ISO/IEC-10646)", + "scheme-pictogram-encoding": "9 - Pictogram Encoding", + "scheme-music-codes": "10 - Music Codes (ISO-2022-JP)", + "scheme-extended-kanji-jis": "13 - Extended Kanji JIS (X 0212-1990)", + "scheme-korean-graphic-character-set": "14 - Korean Graphic Character Set (KS C 5601/KS X 1001)" + }, + "queue-select-name": "选择队列名称", + "queue-name": "名称", + "queue-name-required": "队列名称必填!", + "queues": "队列", + "queue-partitions": "分区", + "queue-submit-strategy": "提交策略", + "queue-processing-strategy": "处理策略", + "queue-configuration": "队列配置", + "repository-settings": "仓库设置", + "repository-url": "仓库地址", + "repository-url-required": "仓库地址必填。", + "default-branch": "默认分支", + "repository-read-only": "Read-only", + "authentication-settings": "认证设置", + "auth-method": "认证方法", + "auth-method-username-password": "密码/令牌", + "auth-method-private-key": "私钥", + "password-access-token": "密码/令牌", + "change-password-access-token": "修改密码/令牌", + "private-key": "私钥", + "drop-private-key-file-or": "拖拽私钥或", + "passphrase": "密码", + "enter-passphrase": "输入密码", + "change-passphrase": "更改密码", + "check-access": "验证", + "check-repository-access-success": "验证仓库访问成功!", + "delete-repository-settings-title": "你确定要删除仓库设置吗?", + "delete-repository-settings-text": "确认删除仓库设置后版本控制功能会失效。", + "auto-commit-settings": "自动提交设置", + "auto-commit-entities": "自动提交实体", + "no-auto-commit-entities-prompt": "没有实体配置自动提交", + "delete-auto-commit-settings-title": "你是否确定要删除实体自动提交配置吗?", + "delete-auto-commit-settings-text": "确认删除后自动提交设置对所有实体失效。", + "2fa": { + "2fa": "双因素认证", + "available-providers": "认证供应商", + "issuer-name": "名称", + "issuer-name-required": "名称必填。", + "max-verification-failures-before-user-lockout": "验证失败次数", + "max-verification-failures-before-user-lockout-pattern": "验证失败次数必须是一个正整数。", + "number-of-checking-attempts": "尝试检查次数", + "number-of-checking-attempts-pattern": "尝试检查次数必须是一个正整数。", + "number-of-checking-attempts-required": "需要检查的次数。", + "number-of-codes": "数字代码", + "number-of-codes-pattern": "数字代码必须是一个正整数。", + "number-of-codes-required": "数字代码必填。", + "provider": "提供程序", + "retry-verification-code-period": "重试时间(秒)", + "retry-verification-code-period-pattern": "最小时间为5秒", + "retry-verification-code-period-required": "重试时间必填。", + "total-allowed-time-for-verification": "验证时间(秒)", + "total-allowed-time-for-verification-pattern": "允许验证时间为60秒", + "total-allowed-time-for-verification-required": "允许验证时间必填。", + "use-system-two-factor-auth-settings": "使用系统双重因子验证设置", + "verification-code-check-rate-limit": "验证代码检查限制", + "verification-code-lifetime": "验证代码周期(秒)", + "verification-code-lifetime-pattern": "验证代码周期(秒)必须是一个正整数。", + "verification-code-lifetime-required": "验证代码周期(秒)必填。", + "verification-message-template": "验证消息模板", + "verification-limitations": "验证限制", + "verification-message-template-pattern": "验证消息需要包含模式:${code}", + "verification-message-template-required": "验证消息模板必填。", + "within-time": "时间限制(秒)", + "within-time-pattern": "时间限制必须是一个正整数。", + "within-time-required": "时间限制必填。" + } + }, + "alarm": { + "alarm": "警报", + "alarms": "警报", + "select-alarm": "选择警报", + "no-alarms-matching": "没有找到匹配'{{entity}}'的警报", + "alarm-required": "警报必填", + "alarm-status": "警报状态", + "alarm-status-list": "报警状态列表", + "any-status": "任何状态", + "search-status": { + "ANY": "所有", + "ACTIVE": "激活", + "CLEARED": "已清除", + "ACK": "已确认", + "UNACK": "未确认" + }, + "display-status": { + "ACTIVE_UNACK": "激活未确认", + "ACTIVE_ACK": "激活已确认", + "CLEARED_UNACK": "清除未确认", + "CLEARED_ACK": "清除已确认" + }, + "no-alarms-prompt": "未发现警报", + "created-time": "创建时间", + "type": "类型", + "severity": "严重程度", + "originator": "发起者", + "originator-type": "发起者类型", + "details": "详情", + "status": "状态", + "alarm-details": "报警详细信息", + "start-time": "开始时间", + "end-time": "结束时间", + "ack-time": "确认时间", + "clear-time": "清除时间", + "alarm-severity-list": "警报严重性列表", + "any-severity": "任何严重程度", + "severity-critical": "危险", + "severity-major": "重要", + "severity-minor": "次要", + "severity-warning": "警报", + "severity-indeterminate": "不确定", + "acknowledge": "应答", + "clear": "清除", + "search": "查找警报", + "selected-alarms": "已选择{ count, plural, 1 {1个警报} other {#个警报} }", + "no-data": "无数据显示", + "polling-interval": "警报轮询间隔(秒)", + "polling-interval-required": "警报轮询间隔必填。", + "min-polling-interval-message": "轮询间隔至少是1秒。", + "aknowledge-alarms-title": "确认{ count, plural, 1 {1个警报 } other {#个警报} }", + "aknowledge-alarms-text": "确定要确认{ count, plural, 1 {1个警报} other {# 个警报} }?", + "aknowledge-alarm-title": "确认报警", + "aknowledge-alarm-text": "确定要确认报警吗?", + "clear-alarms-title": "清除{count, plural, 1 {1个警报} other { #个警报} }", + "clear-alarms-text": "确定要清除{count, plural, 1 {1个警报} other {#个警报} }?", + "clear-alarm-title": "清除警报", + "clear-alarm-text": "确定要清除警报吗?", + "alarm-status-filter": "报警状态过滤器", + "alarm-filter": "报警过滤器", + "max-count-load": "要加载的最大报警数(0-无限制)", + "max-count-load-required": "加载的最大报警数必填。", + "max-count-load-error-min": "最小值为0。", + "fetch-size": "获取大小", + "fetch-size-required": "获取大小必填。", + "fetch-size-error-min": "最小值为10。", + "alarm-type-list": "报警类型列表", + "any-type": "任何类型", + "search-propagated-alarms": "检索已传递的警报" + }, + "alias": { + "add": "添加别名", + "edit": "编辑别名", + "name": "别名", + "name-required": "别名必填", + "duplicate-alias": "别名已经存在。", + "filter-type-single-entity": "单个实体", + "filter-type-entity-list": "实体列表", + "filter-type-entity-name": "实体名称", + "filter-type-entity-type": "实体类型", + "filter-type-state-entity": "仪表板实体状态", + "filter-type-state-entity-description": "仪表板实体令牌状态参数", + "filter-type-asset-type": "资产类型", + "filter-type-asset-type-description": "类型为'{{assetType}}'的资产", + "filter-type-asset-type-and-name-description": "类型为'{{assetType}}'且以'{{prefix}}'开头的资产", + "filter-type-device-type": "设备类型", + "filter-type-device-type-description": "类型为'{{deviceType}}'的设备", + "filter-type-device-type-and-name-description": "类型为'{{deviceType}}'且以'{{prefix}}'开头的设备", + "filter-type-entity-view-type": "实体视图类型", + "filter-type-entity-view-type-description": "类型为'{{entityView}}'的实体视图", + "filter-type-entity-view-type-and-name-description": "类型为{{entityView}}'且以'{{prefix}}'开头的实体视图", + "filter-type-edge-type": "Edge类型", + "filter-type-edge-type-description": "类型为'{{edgeType}}'的Edge", + "filter-type-edge-type-and-name-description": "类型为'{{edgeType}}'且以'{{prefix}}'开头的Edge", + "filter-type-relations-query": "关联查询", + "filter-type-relations-query-description": "{{entities}}具有{{relationType}}关联{{direction}}的{{rootEntity}}", + "filter-type-asset-search-query": "资产查询", + "filter-type-asset-search-query-description": "类型为{{assetTypes}}且具有{{relationType}}关联{{direction}}的{{rootEntity}}资产", + "filter-type-device-search-query": "设备查询", + "filter-type-device-search-query-description": "类型为{{deviceTypes}}且具有{{relationType}}关联{{direction}}的{{rootEntity}}设备", + "filter-type-entity-view-search-query": "实体视图查询", + "filter-type-entity-view-search-query-description": "类型为{{entityViewTypes}}且具有{{relationType}}关联{{direction}}的{{rootEntity}}实体视图", + "filter-type-apiUsageState": "Api统计状态", + "filter-type-edge-search-query": "edge查询", + "filter-type-edge-search-query-description": "类型为{{edgeTypes}}且具有{{relationType}}关联{{direction}}的{{rootEntity}}Edge", + "entity-filter": "实体过滤器", + "resolve-multiple": "解析为多个实体", + "filter-type": "过滤器类型", + "filter-type-required": "过滤器类型必填。", + "entity-filter-no-entity-matched": "未找到符合指定筛选条件的实体。", + "no-entity-filter-specified": "没有指定实体过滤器", + "root-state-entity": "使用仪表板状态实体作为根实体", + "last-level-relation": "仅获取最后一级关联", + "root-entity": "根实体", + "state-entity-parameter-name": "状态实体参数名称", + "default-state-entity": "默认状态实体", + "default-entity-parameter-name": "默认", + "max-relation-level": "最大关联层级", + "unlimited-level": "不限层级", + "state-entity": "仪表板状态实体", + "all-entities": "所有实体", + "any-relation": "不限" + }, + "asset": { + "asset": "资产", + "assets": "资产", + "management": "资产管理", + "view-assets": "查看资产", + "add": "添加资产", + "asset-type-max-length": "资产类型要小于256", + "assign-to-customer": "分配给客户", + "assign-asset-to-customer": "将资产分配给客户", + "assign-asset-to-customer-text": "请选择要分配给客户的资产", + "no-assets-text": "未找到资产", + "assign-to-customer-text": "请选择客户以分配资产", + "public": "公开", + "assignedToCustomer": "分配客户", + "make-public": "资产设为公开", + "make-private": "资产设为私有", + "unassign-from-customer": "取消分配客户", + "delete": "删除资产", + "asset-public": "资产公开", + "asset-type": "资产类型", + "asset-type-required": "资产类型必填。", + "select-asset-type": "选择资产类型", + "enter-asset-type": "输入资产类型", + "any-asset": "任何资产", + "no-asset-types-matching": "没有找到匹配'{{entitySubtype}}'的资产类型。", + "asset-type-list-empty": "资产类型未选择。", + "asset-types": "资产类型", + "name": "名称", + "name-required": "名称必填。", + "name-max-length": "名称长度不能大于256", + "label-max-length": "标签长度应小于256", + "description": "描述", + "type": "类型", + "type-required": "类型必填。", + "details": "详情", + "events": "事件", + "add-asset-text": "添加新资产", + "asset-details": "资产详情", + "assign-assets": "分配资产", + "assign-assets-text": "分配{ count, plural, 1 {1个资产} other {#个资产} }给客户", + "assign-asset-to-edge-title": "将资产分配给Edge", + "assign-asset-to-edge-text": "请选择要分配给Edge的资产", + "delete-assets": "删除资产", + "unassign-assets": "取消分配资产", + "unassign-assets-action-title": "从客户处取消分配{ count, plural, 1 {1个资产} other {#个资产} }", + "assign-new-asset": "分配新资产", + "delete-asset-title": "确定要删除资产'{{assetName}}'?", + "delete-asset-text": "请注意:确认后资产数据将无法恢复。", + "delete-assets-title": "确定要删除 { count, plural, 1 {1个资产} other {#个资产} }?", + "delete-assets-action-title": "删除 { count, plural, 1 {1个资产} other {#个资产} }", + "delete-assets-text": "请注意:确认删除后资产后数据将无法恢复。", + "make-public-asset-title": "你确定你想创建公开'{{assetName}}'资产?", + "make-public-asset-text": "确认后资产数据被公开并允许他人访问。", + "make-private-asset-title": "你确定你想创建私有'{{assetName}}'资产?", + "make-private-asset-text": "确认后资产数据被公开并允许他人访问。", + "unassign-asset-title": "你确定要取消对'{{assetName}}'资产的分配吗?", + "unassign-asset-text": "确认后资产取消分配并且客户无法访问。", + "unassign-asset": "未分配资产", + "unassign-assets-title": "你确定要取消分配{ count, plural, 1 {1个资产} other {#个资产} }吗?", + "unassign-assets-text": "确认后资产将取消分配并且客户无法访问。", + "unassign-assets-from-edge": "取消分配Edge", + "copyId": "复制资产ID", + "idCopiedMessage": "资产ID已经复制到粘贴板", + "select-asset": "选择资产", + "no-assets-matching": "没有找到匹配'{{entity}}'的资产。", + "asset-required": "资产必填", + "name-starts-with": "资产名称以此开头", + "help-text": "根据需要可以使用'%'进行匹配,例如:'%asset_name_contains%', '%asset_name_ends', 'asset_starts_with'。", + "import": "导入资产", + "asset-file": "资产档案", + "label": "标签", + "search": "查找资产", + "assign-asset-to-edge": "将资产分配给Edge", + "unassign-asset-from-edge": "取消分配Edge", + "unassign-asset-from-edge-title": "你确定要取消对'{{assetName}}'资产的分配吗?", + "unassign-asset-from-edge-text": "确认后资产将取消分配并且Edge无法访问", + "unassign-assets-from-edge-title": "你确定要取消分配{ count, plural, 1 {1个资产} other {#个资产} }吗?", + "unassign-assets-from-edge-text": "确认后资产将取消分并且Edge无法访问", + "selected-assets": "已选择{ count, plural, 1 {1个资产} other {#个资产} }" + }, + "attribute": { + "attributes": "属性", + "latest-telemetry": "最新遥测数据", + "attributes-scope": "设备属性范围", + "scope-latest-telemetry": "最新遥测数据", + "scope-client": "客户端属性", + "scope-server": "服务端属性", + "scope-shared": "共享属性", + "add": "添加属性", + "key": "数据键", + "key-max-length": "关键字长度应小于256", + "last-update-time": "最后更新时间", + "key-required": "属性键必填。", + "value": "数据值", + "value-required": "属性值必填。", + "delete-attributes-title": "你确定要删除{ count, plural, 1 {1个属性} other {#个属性} }吗?", + "delete-attributes-text": "注意确认后所有属性都会被删除。", + "delete-attributes": "删除属性", + "enter-attribute-value": "输入属性值", + "show-on-widget": "显示到部件", + "widget-mode": "部件模式", + "next-widget": "下一个部件", + "prev-widget": "上一个部件", + "add-to-dashboard": "添加到仪表板", + "add-widget-to-dashboard": "将部件添加到仪表板", + "selected-attributes": "已选择{ count, plural, 1 {1个属性} other {#个属性} }", + "selected-telemetry": "已选择{ count, plural, 1 {1个遥测} other {#个遥测} }", + "no-attributes-text": "找不到属性", + "no-telemetry-text": "未找到遥测数据" + }, + "api-usage": { + "api-usage": "Api统计", + "alarm": "警报", + "alarms-created": "创建警报数", + "alarms-created-daily-activity": "每天产生的警报", + "alarms-created-hourly-activity": "每小时产生的警报", + "alarms-created-monthly-activity": "每月产生的警报", + "data-points": "数据点", + "data-points-storage-days": "日存储数据点数", + "email": "邮件", + "email-messages": "邮件信息", + "email-messages-daily-activity": "每天产生的邮件信息", + "email-messages-monthly-activity": "每月产生的邮件信息", + "exceptions": "例外", + "executions": "执行", + "javascript": "JavaScript", + "javascript-executions": "JavaScript例外", + "javascript-functions": "JavaScript函数", + "javascript-functions-daily-activity": "J每天执行的JavaScript函数", + "javascript-functions-hourly-activity": "每小时执行的JavaScript函数", + "javascript-functions-monthly-activity": "每月执行的JavaScript函数", + "latest-error": "最新错误", + "messages": "消息", + "notifications": "通知", + "notifications-email-sms": "通知(Email/SMS)", + "notifications-hourly-activity": "每小时的通知", + "permanent-failures": "${entityName}永久性故障", + "permanent-timeouts": "${entityName}永久超时", + "processing-failures": "${entityName}处理失败", + "processing-failures-and-timeouts": "处理失败和超时", + "processing-timeouts": "${entityName}处理超时", + "queue-stats": "队列统计信息", + "rule-chain": "规则链", + "rule-engine": "规则引擎", + "rule-engine-daily-activity": "每天的规则引擎活动", + "rule-engine-executions": "规则引擎执行", + "rule-engine-hourly-activity": "每小时的规则引擎活动", + "rule-engine-monthly-activity": "每月的规则引擎活动", + "rule-engine-statistics": "规则引擎统计信息", + "rule-node": "规则节点", "sms": "SMS", - "sms-description": "使用短信进行身份验证。当登录时,系统会通过短信向您发送验证码。", - "sms-hint": "身份验证码通过短信发送到 {{ info }}", - "totp": "验证APP", - "totp-description": "使用手机上的Google Authenticator、Authy或Duo等应用程序进行身份验证,它将生成用于登录的验证码。", - "totp-hint": "已为您的帐户设置验证APP", - "backup_code": "备份验证码", - "backup-code-description": "这些可打印的一次性密码允许您在离开手机时登录,比如正在旅行。", - "backup-code-hint": "{{ info }}个一次性代码处于激活状态" - } - }, - "password-requirement": { - "at-least": "至少:", - "character": "{ count, plural, 1 {1 位字符} other {# 位字符} }", - "digit": "{ count, plural, 1 {1 位数字} other {# 位数字} }", - "incorrect-password-try-again": "密码不正确。再试一次", - "lowercase-letter": "{ count, plural, 1 {1 位小写字母} other {# 位小写字母} }", - "new-passwords-not-match": "新密码不匹配", - "password-should-not-contain-spaces": "密码不应包含空格", - "password-not-meet-requirements": "密码不符合要求", - "password-requirements": "密码要求", - "password-should-difference": "新密码应与当前密码不同", - "special-character": "{ count, plural, 1 {1 位特殊字符} other {# 位特殊字符} }", - "uppercase-letter": "{ count, plural, 1 {1 位大写字母} other {# 位大写字母} }" - } - }, - "queue": { - "queue-name": "队列", - "no-queues-found": "未找到队列", - "no-queues-matching": "未找到匹配 '{{queue}}' 的队列", - "select-name": "选择队列名称", - "name": "名称", - "name-required": "队列名称必填。", - "name-unique": "队列名称必须唯一。", - "name-pattern": "队列名称不能包含ASCII字母数字以外的字符, '.', '_' 和 '-'等。", - "queue-required": "队列必填。", - "topic-required": "队列主题必填。", - "poll-interval-required": "轮询间隔必填。", - "poll-interval-min-value": "轮询间隔不能小于1", - "partitions-required": "分区必填。", - "partitions-min-value": "分区不能小于1", - "pack-processing-timeout-required": "处理超时时间必填。", - "pack-processing-timeout-min-value": "处理超时时间不能小于1", - "batch-size-required": "批量处理大小必填。", - "batch-size-min-value": "批量处理大小不能小于1", - "retries-required": "重试次数必填。", - "retries-min-value": "重试次数不能为负", - "failure-percentage-required": "失败百分比必填。", - "failure-percentage-min-value": "失败百分比值不能小于0", - "failure-percentage-max-value": "失败百分比值不能大于100", - "pause-between-retries-required": "重试间隔必填。", - "pause-between-retries-min-value": "重试间隔不能小于1", - "max-pause-between-retries-required": "最大重试间隔必填。", - "max-pause-between-retries-min-value": "最大重试间隔不能小于1", - "submit-strategy-type-required": "提交策略类型必填。", - "processing-strategy-type-required": "处理策略类型必填。", - "queues": "队列", - "selected-queues": "已选择 { count, plural, 1 {1 个队列} other {# 个队列} }", - "delete-queue-title": "确定要删除 '{{queueName}}' 队列吗?", - "delete-queues-title": "确定要删除 { count, plural, 1 {1 个队列} other {# 个队列} }吗?", - "delete-queue-text": "请注意:确认后,队列和所有相关数据将不可恢复。", - "delete-queues-text": "确认后,所有选定队列都将被删除,无法访问。", - "search": "搜索队列", - "add" : "添加队列", - "details": "队列详情", - "topic": "主题", - "submit-settings": "提交设置", - "submit-strategy": "策略类型*", - "grouping-parameter": "分组参数", - "processing-settings": "重试处理设置", - "processing-strategy": "处理类型*", - "retries-settings": "重试设置", - "polling-settings": "轮询设置", - "batch-processing": "批量处理", - "poll-interval": "轮询间隔", - "partitions": "分区", - "immediate-processing": "即时处理", - "consumer-per-partition": "每个分区消费者单独轮询消息", - "consumer-per-partition-hint": "每个分区启用单独的消费者", - "processing-timeout": "处理超时(毫秒)", - "batch-size": "批量处理大小", - "retries": "重试次数 (0 – 无限制)", - "failure-percentage": "跳过重试的失败消息百分比", - "pause-between-retries": "重试间隔(秒)", - "max-pause-between-retries": "最大重试间隔(秒)", - "delete": "删除队列", - "copyId": "复制队列ID", - "idCopiedMessage": "队列ID已复制到剪贴板", - "description": "说明", - "description-hint": "此文本将显示在队列说明中,而不是所选策略中", - "alt-description": "提交策略:{{submitStrategy}},处理策略:{{processingStrategy}}", - "strategies": { - "sequential-by-originator-label": "按发起者顺序处理", - "sequential-by-originator-hint": "在确认设备A的前一条消息之前,不会提交设备A的新消息", - "sequential-by-tenant-label": "按租户顺序处理", - "sequential-by-tenant-hint": "在确认租户A的前一条消息之前,不会提交租户A的新消息", - "sequential-label": "顺序处理", - "sequential-hint": "在确认前一条消息之前,不会提交新消息", - "burst-label": "突发处理", - "burst-hint": "所有消息都按到达的顺序提交到规则链", - "batch-label": "批量处理", - "batch-hint": "在确认前一批消息之前,不会提交新批", - "skip-all-failures-label": "跳过所有失败", - "skip-all-failures-hint": "忽略所有失败", - "skip-all-failures-and-timeouts-label": "跳过所有失败和超时", - "skip-all-failures-and-timeouts-hint": "忽略所有失败和超时", - "retry-all-label": "全部重试", - "retry-all-hint": "重试处理包中的所有消息", - "retry-failed-label": "失败重试", - "retry-failed-hint": "重试处理包中的所有失败消息", - "retry-timeout-label": "超时重试", - "retry-timeout-hint": "重试处理包中的所有超时消息", - "retry-failed-and-timeout-label": "失败与超时重试", - "retry-failed-and-timeout-hint": "重试处理包中所有失败和超时的消息" - } - }, - "relation": { - "add": "添加关联", - "add-relation-filter": "添加关联筛选器", - "additional-info": "附加信息 (JSON)", - "any-relation": "任意关联", - "any-relation-type": "任何类型", - "delete": "删除关联", - "delete-from-relation-text": "确定删除后,当前实体将与实体 '{{entityName}}' 取消关联", - "delete-from-relation-title": "确定要从实体 '{{entityName}}' 删除关联吗?", - "delete-from-relations-text": "确定删除所有选择的关联关系后,当前实体将与对应的实体取消关联", - "delete-from-relations-title": "确定删除 { count, plural, 1 {# 个关联} other {# 个关联} } 吗?", - "delete-to-relation-text": "确定删除后实体 '{{entityName}}' 将取消与当前实体的关联关系。", - "delete-to-relation-title": "确定要删除实体 '{{entityName}}' 的关联吗?", - "delete-to-relations-text": "确定删除所有选择的关联关系后,与当前实体对应的所有关联关系将被移除。", - "delete-to-relations-title": "确定要删除 { count, plural, 1 {# 个关联} other {# 个关联} }吗?", - "direction": "方向", - "direction-type": { - "FROM": "从", - "TO": "到" - }, - "edit": "编辑关联", - "from-entity": "从实体", - "from-entity-name": "从实体类型", - "from-entity-type": "从实体类型", - "from-relations": "向外的关联", - "invalid-additional-info": "无法解析附加信息JSON。", - "no-relations-text": "未找到关联", - "relation-filters": "关联筛选器", - "relation-type": "关联类型", - "relation-type-required": "关联类型必填", - "relations": "关联", - "remove-relation-filter": "移除关联筛选器", - "search-direction": { - "FROM": "从", - "TO": "到" - }, - "selected-relations": "已选择{ count, plural, 1 {# 个关联} other {# 个关联} }", - "to-entity": "到实体", - "to-entity-name": "到实体名称", - "to-entity-type": "到实体类型", - "to-relations": "向内的关联", - "type": "类型" - }, - "resource": { - "add": "添加资源", - "delete": "删除资源", - "delete-resource-text": "请注意:确认后,资源将不可恢复。", - "delete-resource-title": "确定要删除资源 '{{resourceTitle}}' 吗?", - "delete-resources-action-title": "删除 { count, plural, 1 {# 个资源} other {# 个资源} }", - "delete-resources-text": "请注意:确认后,所有选定的资源将被删除。", - "delete-resources-title": "确定要删除 { count, plural, 1 {# 个资源} other {# 个资源} }吗?", - "drop-file": "拖拽资源文件或单击以选择要上传的文件。", - "empty": "资源为空", - "no-resource-matching": "未找到与 '{{widgetsBundle}}' 匹配的资源。", - "no-resource-text": "未找到资源", - "open-widgets-bundle": "打开部件库", - "resource": "资源", - "resource-library-details": "资源库详情", - "resource-type": "资源类型", - "resources-library": "资源库", - "search": "查找资源", - "selected-resources": "已选择{ count, plural, 1 {# 个资源} other {# 个资源} }", - "system": "系统", - "title": "标题", - "title-required": "标题是必填项。" -}, - "rulechain": { - "add": "添加规则链", - "add-rulechain-text": "添加新的规则链", - "assign-to-edge": "分配给边缘", - "assign-rulechain-to-edge-title": "分配规则链给边缘", - "assign-rulechain-to-edge-text": "请选择要分配给边缘的规则链", - "copyId": "复制规则链ID", - "create-new-rulechain": "创建新的规则链", - "debug-mode": "调试模式", - "delete": "删除规则链", - "delete-rulechain-text": "请注意,确认后,规则链和所有相关数据将不可恢复。", - "delete-rulechain-title": " 确定要删除规则链'{{ruleChainName}}'吗?", - "delete-rulechains-action-title": "删除 { count, plural, 1 {# 个规则链} other {# 个规则链} }", - "delete-rulechains-text": "请注意:确认后,所有选定的规则链将被删除,所有相关的数据将不可恢复。", - "delete-rulechains-title": "确定要删除{count, plural, 1 { 1 个规则链} other {# 个规则链} }吗?", - "description": "说明", - "details": "详情", - "edge-rulechain": "边缘规则链", - "edge-template-root": "模版根链", - "events": "事件", - "export": "导出规则链", - "export-failed-error": "无法导出规则链:{{error}}", - "idCopiedMessage": "规则ID已经复制到粘贴板", - "import": "导入规则链", - "invalid-rulechain-file-error": "不能导入规则链:无效的规则链数据格式。", - "invalid-rulechain-type-error": "不能导入规则链:无效的规则链类型。期望类型为{{expectedRuleChainType}}。", - "management": "规则集管理", - "name": "名称", - "name-required": "名称必填。", - "no-rulechains-matching": "没有发现匹配'{{entity}}'的规则链。", - "no-rulechains-text": "未找到规则链", - "open-rulechain": "打开规则链", - "root": "是否根链", - "rulechain": "规则链", - "rulechain-details": "规则链详情", - "rulechain-file": "规则链文件", - "rulechain-required": "规则链必填", - "rulechains": "规则链库", - "search": "查找规则链", - "select-rulechain": "选择规则链", - "selected-rulechains": "已选择 { count, plural, 1 {# 个规则链} other {# 个规则链} }", - "set-auto-assign-to-edge": "将规则链分配给新创建的边缘", - "set-auto-assign-to-edge-text": "确认后,将自动分配规则链给新创建的边缘。", - "set-auto-assign-to-edge-title": "确定将规则链'{{ruleChainName}}'自动分配给新创建的边缘吗?", - "set-edge-template-root-rulechain": "设置为边缘模版根规则链", - "set-edge-template-root-rulechain-text": "确认后,将会成为边缘模版根规则链,且它会成为新创建边缘的根规则链。", - "set-edge-template-root-rulechain-title": "确定将 '{{ruleChainName}}' 设置为边缘模版根规则链吗?", - "set-root": "设置为根规则链", - "set-root-rulechain-text": "确认后,规则链将变为根规格链,并将处理所有传入的传输消息。", - "set-root-rulechain-title": "确定要设置'{{ruleChainName}}'为根规则链吗?", - "system": "系统", - "unassign-rulechain-from-edge-text": "确认后,规则链将会取消分配,边缘无法访问。", - "unassign-rulechains-from-edge-text": "确认后,选定的规则链将会取消分配,边缘无法访问。", - "unassign-rulechains-from-edge-title": "确定要取消分配 { count, plural, 1 {1 个规则链} other {# 个规则链} }吗?", - "unset-auto-assign-to-edge": "不将规则链分配给边缘", - "unset-auto-assign-to-edge-text": "确认后,将不再自动分配规则链给新创建的边缘。", - "unset-auto-assign-to-edge-title": "确定不再将规则链'{{ruleChainName}}'自动分配给新创建的边缘吗?" - }, - "rulenode": { - "add": "添加规则节点", - "add-link": "添加链接", - "configuration": "配置", - "copy-selected": "选择副本", - "create-new-link-label": "创建一个新的!", - "custom-link-label": "自定义链接标签", - "custom-link-label-required": "自定义链接标签必填", - "debug-mode": "调试模式", - "delete": "删除规则节点", - "delete-selected": "删除选定", - "delete-selected-objects": "删除选定的节点和连接", - "description": "说明", - "deselect-all": "取消选择", - "deselect-all-objects": "取消选择所有节点和连接", - "details": "详情", - "directive-is-not-loaded": "定义的配置指令 '{{directiveName}}' 不可用。", - "events": "事件", - "help": "帮助", - "invalid-target-rulechain": "无法解析目标规则链!", - "link": "链接", - "link-details": "规则节点链接详情", - "link-label": "链接标签", - "link-label-required": "链接标签必填", - "link-labels": "链接标签", - "link-labels-required": "链接标签必填。", - "message": "消息", - "message-type": "消息类型", - "message-type-required": "消息类型必填", - "metadata": "元数据", - "metadata-required": "元数据项不能为空。", - "name": "名称", - "name-required": "名称必填。", - "no-link-label-matching": "未找到匹配 '{{label}}' 的链接标签。", - "no-link-labels-found": "未找到链接标签", - "open-node-library": "打开节点库", - "output": "输出", - "reset-debug-mode": "重置所有节点中的调试模式", - "rulenode-details": "规则节点详情", - "search": "查找节点", - "select-all": "选择全部", - "select-all-objects": "选择所有节点和连接", - "select-message-type": "选择消息类型", - "test": "测试", - "test-script-function": "测试脚本功能", - "type": "类型", - "type-action": "动作", - "type-action-details": "执行特别动作", - "type-enrichment": "属性集", - "type-enrichment-details": "向消息元数据中添加附加信息", - "type-external": "外部的", - "type-external-details": "与外部系统交互", - "type-filter": "筛选器", - "type-filter-details": "使用配置条件筛选传入消息", - "type-input": "输入", - "type-input-details": "规则链的逻辑输入,将传入消息转发到下一个相关规则节点", - "type-rule-chain": "规则链", - "type-rule-chain-details": "将传入消息转发到指定的规则链", - "type-transformation": "变换", - "type-transformation-details": "更改消息 Payload 和元数据", - "type-unknown": "未知", - "type-unknown-details": "未解析的规则节点", - "ui-resources-load-error": "加载配置UI资源失败。" - }, - "tenant-profile": { - "tenant-profile": "租户配置", - "tenant-profiles": "租户配置", - "add": "添加租户配置", - "edit": "编辑租户配置", - "tenant-profile-details": "租户配置详细信息", - "no-tenant-profiles-text": "未找到租户配置", - "name-max-length": "名称长度必须少于256个字符", - "search": "查找租户配置", - "selected-tenant-profiles": "已选择 { count, plural, 1 {# 个租户配置} other {# 个租户配置} }", - "no-tenant-profiles-matching": "未找到与 '{{entity}}' 匹配的租户配置。", - "tenant-profile-required": "租户配置必填", - "idCopiedMessage": "租户配置ID已复制到剪贴板", - "set-default": "设置该租户配置为默认", - "delete": "删除租户配置", - "copyId": "复制租户配置ID", - "name": "名称", - "name-required": "名称必填。", - "data": "配置数据", - "profile-configuration": "配置设置", - "description": "说明", - "default": "默认", - "delete-tenant-profile-title": "确定要删除租户配置 '{{tenantProfileName}}'吗?", - "delete-tenant-profile-text": "请注意:确认后,租户配置和所有相关数据将不可恢复。", - "delete-tenant-profiles-title": "确定要删除 { count, plural, 1 {# 个租户配置} other {# 个租户配置} }吗?", - "delete-tenant-profiles-text": "请注意:确认后,所有选定的租户配置将被删除,所有相关数据将不可恢复。", - "set-default-tenant-profile-title": "确定要将租户配置 '{{tenantProfileName}}' 设为默认值吗?", - "set-default-tenant-profile-text": "确认后,此租户配置将被标记为默认配置,并将用于未指定配置的新租户。", - "no-tenant-profiles-found": "未找到租户配置。", - "create-new-tenant-profile": "创建一个新的!", - "create-tenant-profile": "创建新的租户配置", - "import": "导入租户配置", - "export": "导出租户配置", - "export-failed-error": "无法导出租户配置: {{error}}", - "tenant-profile-file": "租户配置文件", - "invalid-tenant-profile-file-error": "无法导入租户配置:无效的租户配置数据结构。", - "advanced-settings": "高级设置", - "entities": "实体", - "rule-engine": "规则引擎", - "time-to-live": "TTL", - "alarms-and-notifications": "告警与通知", - "ota-files-in-bytes": "OTA文件(字节)", - "ws-title": "WS", - "unlimited": "(0 - 无限制)", - "maximum-devices": "最大设备数", - "maximum-devices-required": "最大设备数必填。", - "maximum-devices-range": "最大设备数不能为负数", - "maximum-assets": "最大资产数", - "maximum-assets-required": "最大资产数必填。", - "maximum-assets-range": "最大资产数不能为负数", - "maximum-customers": "最大客户数", - "maximum-customers-required": "最大客户数必填。", - "maximum-customers-range": "最大客户数不能为负数", - "maximum-users": "最大用户数", - "maximum-users-required": "最大用户数必填。", - "maximum-users-range": "最大用户数不能为负数", - "maximum-dashboards": "最大仪表板数", - "maximum-dashboards-required": "最大仪表板数必填。", - "maximum-dashboards-range": "最大仪表板数不能为负数", - "maximum-rule-chains": "最大规则链数", - "maximum-rule-chains-required": "最大规则链数必填。", - "maximum-rule-chains-range": "最大规则链数不能为负数", - "maximum-resources-sum-data-size": "资源文件总大小", - "maximum-resources-sum-data-size-required": "资源文件总大小必填。", - "maximum-resources-sum-data-size-range": "资源文件总大小不能为负数", - "maximum-ota-packages-sum-data-size": "OTA包文件总大小", - "maximum-ota-package-sum-data-size-required": "OTA包文件总大小必填。", - "maximum-ota-package-sum-data-size-range": "OTA包文件总大小不能为负数", - "transport-tenant-msg-rate-limit": "租户消息", - "transport-tenant-telemetry-msg-rate-limit": "租户遥测消息", - "transport-tenant-telemetry-data-points-rate-limit": "租户遥测数据点", - "transport-device-msg-rate-limit": "设备消息", - "transport-device-telemetry-msg-rate-limit": "设备遥测数据点", - "transport-device-telemetry-data-points-rate-limit": "设备遥测消息", - "tenant-entity-export-rate-limit": "实体版本创建", - "tenant-entity-import-rate-limit": "实体版本加载", - "max-transport-messages": "最大传输消息数", - "max-transport-messages-required": "最大传输消息数必填。", - "max-transport-messages-range": "最大传输消息数不能为负数", - "max-transport-data-points": "最大传输数据点数", - "max-transport-data-points-required": "最大传输数据点数必填。", - "max-transport-data-points-range": "最大传输数据点数不能为负", - "max-r-e-executions": "最大规则引擎执行数", - "max-r-e-executions-required": "最大规则引擎执行数必填。", - "max-r-e-executions-range": "最大规则引擎执行数不能为负", - "max-j-s-executions": "最大 JavaScript 执行数", - "max-j-s-executions-required": "最大 JavaScript 执行数必填。", - "max-j-s-executions-range": "最大 JavaScript 执行数不能为负数", - "max-d-p-storage-days": "最大存储点天", - "max-d-p-storage-days-required": "最大存储点天必填。", - "max-d-p-storage-days-range": "最大存储点天不能为负数", - "default-storage-ttl-days": "默认存储TTL天数", - "default-storage-ttl-days-required": "默认存储TTL天数必填。.", - "default-storage-ttl-days-range": "默认存储TTL天数不能为负数", - "alarms-ttl-days": "告警TTL天数", - "alarms-ttl-days-required": "告警TTL天数必填。", - "alarms-ttl-days-days-range": "告警TTL天数不能为负数", - "rpc-ttl-days": "RPC TTL天数", - "rpc-ttl-days-required": "RPC TTL天数必填。", - "rpc-ttl-days-days-range": "RPC TTL天数不能为负数", - "max-rule-node-executions-per-message": "每条消息的最大规则节点执行数", - "max-rule-node-executions-per-message-required": "每个消息的最大规则节点执行数必填。", - "max-rule-node-executions-per-message-range": "每条消息的最大规则节点执行数不能为负", - "max-emails": "最大电子邮件发送数", - "max-emails-required": "最大电子邮件发送数必填。", - "max-emails-range": "最大电子邮件发送数不能为负", - "max-sms": "最大短信发送数", - "max-sms-required": "最大短信发送数必填。", - "max-sms-range": "最大短信发送数不能为负", - "max-created-alarms": "最大创建告警数", - "max-created-alarms-required": "最大创建告警数必填。", - "max-created-alarms-range": "最大创建告警数不能为负数", - "no-queue": "未配置队列", - "add-queue": "添加队列", - "queues-with-count": "队列 ({{count}})", - "tenant-rest-limits": "租户REST请求", - "customer-rest-limits": "客户REST请求", - "incorrect-pattern-for-rate-limits": "格式为以冒号分割容量与周期(秒)并以逗号分割配置对,例如 100:1,2000:60", - "too-small-value-zero": "数值必须大于0", - "too-small-value-one": "数值必须大于1", - "cassandra-tenant-limits-configuration": "租户Cassandra查询", - "ws-limit-max-sessions-per-tenant": "租户最大会话数", - "ws-limit-max-sessions-per-customer": "客户最大会话数", - "ws-limit-max-sessions-per-regular-user": "普通用户最大会话数", - "ws-limit-max-sessions-per-public-user": "公共用户最大会话数", - "ws-limit-queue-per-session": "会话最大消息队列大小", - "ws-limit-max-subscriptions-per-tenant": "租户最大订阅数", - "ws-limit-max-subscriptions-per-customer": "客户最大订阅数", - "ws-limit-max-subscriptions-per-regular-user": "普通用户最大订阅数", - "ws-limit-max-subscriptions-per-public-user": "公共用户最大订阅数", - "ws-limit-updates-per-session": "会话WS更新", - "rate-limits": { - "add-limit": "添加限制", - "advanced-settings": "高级设置", - "edit-limit": "编辑限制", - "but-less-than": "但小于", - "edit-transport-tenant-msg-title": "编辑传输租户消息速率限制", - "edit-transport-tenant-telemetry-msg-title": "编辑传输租户遥测消息速率限制", - "edit-transport-tenant-telemetry-data-points-title": "编辑传输租户遥测数据点速率限制", - "edit-transport-device-msg-title": "编辑传输设备消息速率限制", - "edit-transport-device-telemetry-msg-title": "编辑传输设备遥测消息速率限制", - "edit-transport-device-telemetry-data-points-title": "编辑传输设备遥测数据点速率限制", - "edit-transport-tenant-msg-rate-limit-title": "编辑传输租户消息速率限制", - "edit-customer-rest-limits-title": "编辑客户REST请求速率限制", - "edit-ws-limit-updates-per-session-title": "编辑会话WS更新速率限制", - "edit-cassandra-tenant-limits-configuration-title": "编辑租户Cassandra查询速率限制", - "edit-tenant-entity-export-rate-limit-title": "编辑实体版本创建速率限制", - "edit-tenant-entity-import-rate-limit-title": "编辑实体版本加载速率限制", - "messages-per": "条消息每", - "not-set": "未配置", - "number-of-messages": "消息数量", - "number-of-messages-required": "消息数量必填。", - "number-of-messages-min": "最小值为1。", - "preview": "预览", - "per-seconds": "每秒", - "per-seconds-required": "时间比率必填。", - "per-seconds-min": "最小值为1。", - "rate-limits": "速率限制", - "remove-limit": "删除限制", - "transport-tenant-msg": "传输租户消息", - "transport-tenant-telemetry-msg": "传输租户遥测消息", - "transport-tenant-telemetry-data-points": "传输租户遥测数据点", - "transport-device-msg": "传输设备消息", - "transport-device-telemetry-msg": "传输设备遥测消息", - "transport-device-telemetry-data-points": "传输设备遥测数据点", - "sec": "秒" - } - }, - "tenant": { - "add": "添加租户", - "add-tenant-text": "添加新租户", - "admins": "管理员", - "copyId": "复制租户ID", - "delete": "删除租户", - "delete-tenant-text": "请注意:确认后,租户和所有相关数据将不可恢复。", - "delete-tenant-title": "确定要删除租户'{{tenantTitle}}'吗?", - "delete-tenants-action-title": "删除 { count, plural, 1 {# 个租户} other {# 个租户} }", - "delete-tenants-text": "请注意:确认后,所有选定的租户将被删除,所有相关数据将不可恢复。", - "delete-tenants-title": "确定要删除 {count,plural,1 {# 个租户} other {# 个租户} } 吗?", - "description": "说明", - "details": "详情", - "events": "事件", - "idCopiedMessage": "租户ID已经复制到粘贴板", - "isolated-tb-rule-engine": "使用独立的规则引擎服务", - "isolated-tb-rule-engine-details": "每个独立租户需要单独的规则引擎微服务", - "manage-tenant-admins": "管理租户管理员", - "management": "租户管理", - "no-tenants-matching": "未找到匹配 '{{entity}}' 的租户", - "no-tenants-text": "未找到租户", - "search": "查找租户", - "select-tenant": "选择租户", - "selected-tenants": "已选择 { count, plural, 1 {# 个租户} other {# 个租户} }", - "tenant": "租户", - "tenant-details": "租客详情", - "tenant-required": "租户必填", - "tenants": "租户", - "title": "标题", - "title-required": "标题必填。" - }, - "timeinterval": { - "advanced": "高级", - "days": "天", - "days-interval": "{ days, plural, 1 {# 天} other {# 天} }", - "hours": "小时", - "hours-interval": "{ hours, plural, 1 {# 小时} other {# 小时} }", - "minutes": "分钟", - "minutes-interval": "{ minutes, plural, 1 {# 分} other {# 分} }", - "seconds": "秒", - "seconds-interval": "{ seconds, plural, 1 {# 秒} other {# 秒} }", - "predefined": { - "yesterday": "昨天", - "day-before-yesterday": "前天", - "this-day-last-week": "前一周的这一天", - "previous-week": "前一周(周日至周六)", - "previous-week-iso": "前一周(周一至周日)", - "previous-month": "前一个月", - "previous-year": "前一年", - "current-hour": "当前小时", - "current-day": "当前天", - "current-day-so-far": "当天到目前为止", - "current-week": "本周(周日至周六)", - "current-week-iso": "本周(周一至周日)", - "current-week-so-far": "本周到目前为止(周日至周六)", - "current-week-iso-so-far": "本周到目前为止(周一至周日)", - "current-month": "本月", - "current-month-so-far": "本月到目前为止", - "current-year": "本年", - "current-year-so-far": "本年到目前为止" - } - }, - "timeunit": { - "days": "天", - "hours": "小时", - "minutes": "分钟", - "seconds": "秒" - }, - "timewindow": { - "date-range": "日期范围", - "days": "{ days, plural, 1 {# 天 } other {# 天 } }", - "edit": "编辑时间窗口", - "hide": "隐藏", - "history": "历史", - "hours": "{ hours, plural, 0 {- 小时 } 1 {# 小时 } other {# 小时 } }", - "last": "最后", - "last-prefix": "最后", - "minutes": "{ minutes, plural, 0 {- 分 } 1 {# 分 } other {# 分 } }", - "period": "从 {{ startTime }} 到 {{ endTime }}", - "realtime": "实时", - "seconds": "{ seconds, plural, 0 {- 秒 } 1 {# 秒 } other {# 秒 } }", - "time-period": "时间段", - "interval": "区间" - }, - "timezone": { - "no-timezones-matching": "No timezones matching '{{timezone}}' were found.", - "select-timezone": "选择时区", - "timezone": "时区", - "timezone-required": "时区必填。" - }, - "user": { - "activation-email-sent-message": "激活电子邮件已成功发送!", - "activation-link": "用户激活链接", - "activation-link-copied-message": "用户激活链接已经复制到粘贴板", - "activation-link-text": "使用该链接 激活 激活用户:", - "activation-method": "激活方式", - "add": "添加用户", - "add-user-text": "添加新用户", - "always-fullscreen": "始终全屏", - "anonymous": "匿名", - "copy-activation-link": "复制用户激活链接", - "customer": "客户", - "customer-users": "客户用户", - "default-dashboard": "默认面板", - "delete": "删除用户", - "delete-user-text": "请注意:确认后,用户和所有相关数据将不可恢复。", - "delete-user-title": "确定要删除用户 '{{userEmail}}' 吗?", - "delete-users-action-title": "删除 { count, plural, 1 {# 个用户} other {# 个用户} }", - "delete-users-text": "请注意:确认后,所有选定的用户将被删除,所有相关数据将不可恢复。", - "delete-users-title": "确定要删除 { count, plural, 1 {# 个用户} other {# 个用户} } 吗?", - "description": "说明", - "details": "详情", - "disable-account": "禁用用户帐户", - "disable-account-message": "已成功禁用用户帐户!", - "display-activation-link": "显示激活链接", - "email": "电子邮件", - "email-required": "电子邮件必填。", - "enable-account": "启用用户帐户", - "enable-account-message": "已成功启用用户帐户!", - "first-name": "名字", - "invalid-email-format": "无效的邮件格式。", - "last-name": "姓氏", - "login-as-customer-user": "以客户用户身份登录", - "login-as-tenant-admin": "以租户管理员身份登录", - "no-users-matching": "未找到匹配 '{{entity}}' 的用户。", - "no-users-text": "未找到用户", - "resend-activation": "重新发送激活", - "search": "查找用户", - "select-user": "选择用户", - "selected-users": "已选择 { count, plural, 1 {# 个用户} other {# 个用户} }", - "send-activation-mail": "发送激活邮件", - "sys-admin": "系统管理员", - "tenant-admin": "租户管理员", - "tenant-admins": "租户管理员", - "user": "用户", - "user-details": "用户详细信息", - "user-required": "用户必填", - "users": "用户" - }, - "value": { - "boolean": "布尔值", - "boolean-value": "布尔值", - "double": "双精度小数", - "double-value": "双精度小数值", - "double-value-required": "需要双精度值", - "false": "假", - "integer": "数字", - "integer-value": "数字值", - "integer-value-required": "整数值必填", - "invalid-integer-value": "整数值无效", - "json": "JSON", - "json-value": "JSON值", - "json-value-invalid": "JSON值的格式无效", - "json-value-required": "JSON值必填。", - "long": "Long", - "string": "字符串", - "string-value": "字符串值", - "string-value-required": "字符串值必填", - "true": "真", - "type": "值类型" - }, - "version-control": { - "version-control": "版本控制", - "management": "版本控制管理", - "search": "搜索版本", - "branch": "分支", - "default": "默认", - "select-branch": "选择分支", - "branch-required": "分支必填", - "create-entity-version": "创建实体版本", - "version-name": "版本名称", - "version-name-required": "版本名称必填", - "author": "作者", - "export-relations": "导出关联", - "export-attributes": "导出属性", - "export-credentials": "导出凭据", - "entity-versions": "实体版本", - "versions": "版本", - "created-time": "创建时间", - "version-id": "版本ID", - "no-entity-versions-text": "未找到实体版本", - "no-versions-text": "未找到版本", - "copy-full-version-id": "复制完整版本ID", - "create-version": "创建版本", - "creating-version": "请稍候,正在创建版本...", - "nothing-to-commit": "没有要提交的更改", - "restore-version": "还原版本", - "restore-entity-from-version": "从版本 '{{versionName}}' 还原实体", - "restoring-entity-version": "请稍候,正在还原实体版本...", - "load-relations": "加载关联", - "load-attributes": "加载属性", - "load-credentials": "加载凭据", - "compare-with-current": "与当前比较", - "diff-entity-with-version": "与实体版本 '{{versionName}}' 不同", - "previous-difference": "上一个差异", - "next-difference": "下一个差异", - "current": "当前", - "differences": "{ count, plural, 1 {1 个差异} other {# 个差异} }", - "create-entities-version": "创建实体版本", - "default-sync-strategy": "默认同步策略", - "sync-strategy-merge": "合并", - "sync-strategy-overwrite": "覆盖", - "entities-to-export": "导出的实体", - "entities-to-restore": "还原的实体", - "sync-strategy": "同步策略", - "all-entities": "所有实体", - "no-entities-to-export-prompt": "请指定要导出的实体", - "no-entities-to-restore-prompt": "请指定要还原的实体", - "add-entity-type": "添加实体类型", - "remove-all": "全部删除", - "version-create-result": "{ added, plural, 0 {没有实体} 1 {1 个实体} other {# 个实体} } 被添加。
{ modified, plural, 0 {没有实体} 1 {1 个实体} other {# 个实体} } 被修改。
{ removed, plural, 0 {没有实体} 1 {1 个实体} other {# 个实体} } 被删除。", - "remove-other-entities": "删除其他实体", - "find-existing-entity-by-name": "按名称查找现有实体", - "restore-entities-from-version": "从版本 '{{versionName}}' 还原实体", - "restoring-entities-from-version": "请稍候,正在还原实体...", - "no-entities-restored": "未还原任何实体", - "created": "{{created}} 创建", - "updated": "{{updated}} 更新", - "deleted": "{{deleted}} 删除", - "remove-other-entities-confirm-text": "请注意!在还原版本中不存在的当前实体
将被永久 删除。

请输入 remove other entities 进行确认。", - "auto-commit-to-branch": "自动提交到 {{ branch }} 分支", - "default-create-entity-version-name": "{{entityName}} 更新", - "sync-strategy-merge-hint": "创建或更新选定的实体,仓库其他实体均不修改。", - "sync-strategy-overwrite-hint": "创建或更新选定的实体,仓库其他实体将被删除。", - "device-credentials-conflict": "无法加载外部ID为 {{entityId}} 的设备
因为数据库中已存在相同的凭据。
请考虑禁用还原表单中的 加载凭据 设置。", - "missing-referenced-entity": "无法加载外部ID为 {{sourceEntityId}} 的 {{sourceEntityTypeName}}
因为它引用了缺失的 {{targetEntityTypeName}} (ID:{{targetEntityId}}).", - "runtime-failed": "失败: {{message}}" - }, - "widget-action": { - "custom": "自定义动作", - "custom-pretty": "自定义操作(使用HTML模板)", - "header-button": "部件顶部按钮", - "open-dashboard": "切换到另一个仪表板", - "open-dashboard-state": "切换到新仪表板状态", - "open-right-layout": "打开右侧布局 (移动端视图)", - "set-entity-from-widget": "从部件中设置实体", - "target-dashboard": "目标仪表板", - "target-dashboard-state": "目标仪表板状态", - "target-dashboard-state-required": "目标仪表板状态必填", - "update-dashboard-state": "更新当前仪表板状态" - }, - "widget-config": { - "action": "Action", - "action-icon": "图标", - "action-name": "名称", - "action-name-not-unique": "Action 名称已经存在。
相同 Action Source 的 Action 名称必须唯一。", - "action-name-required": "Action 名称必填。", - "action-source": "Action Source", - "action-source-required": "Action Source 必填", - "action-type": "类型", - "action-type-required": "类型必填", - "actions": "Actions", - "add-action": "添加 Action", - "add-datasource": "添加数据源", - "advanced": "高级", - "alarm-source": "告警源", - "background-color": "背景颜色", - "data": "数据", - "datasource-parameters": "参数", - "datasource-type": "类型", - "datasources": "数据源", - "decimals": "浮点数后的位数", - "delete-action": "删除 Action", - "delete-action-text": "确定要删除部件Action '{{actionName}}' 吗?", - "delete-action-title": "删除 Widget Action", - "display-icon": "显示标题图标", - "display-legend": "显示图例", - "display-timewindow": "显示时间窗口", - "display-title": "显示标题", - "drop-shadow": "阴影", - "edit-action": "编辑 Action", - "enable-fullscreen": "启用全屏", - "general-settings": "基本设置", - "height": "高度", - "icon-color": "图标颜色", - "icon-size": "图标大小", - "margin": "边缘", - "maximum-datasources": "最大允许 { count, plural, 1 {# 个数据源。} other {# 个数据源。} }", - "mobile-mode-settings": "移动端设置", - "no-actions-text": "未找到 Action", - "order": "顺序", - "padding": "填充", - "remove-datasource": "移除数据源", - "search-actions": "Action 检索", - "settings": "设置", - "target-device": "目标设备", - "text-color": "文字颜色", - "timewindow": "时间窗口", - "title": "标题", - "title-style": "标题风格", - "title-tooltip": "标题工具提示", - "units": "特殊符号展示值", - "use-dashboard-timewindow": "使用仪表板的时间窗口", - "widget-style": "部件风格" - }, - "widget-type": { - "create-new-widget-type": "创建新的部件类型", - "export": "导出部件类型", - "export-failed-error": "无法导出部件类型: {{error}}", - "import": "导入部件类型", - "invalid-widget-type-file-error": "无法导入部件类型:无效的部件类型数据结构。", - "widget-type-file": "部件类型文件" - }, - "widget": { - "add": "添加部件", - "add-resource": "添加资源", - "add-widget-type": "添加新的部件类型", - "alarm": "告警部件", - "alarm-data-overflow": "Widget displays alarms for {{allowedEntities}} (maximum allowed) entities out of {{totalEntities}} entities", - "css": "CSS", - "data-overflow": "Widget displays {{count}} out of {{total}} entities", - "datakey-settings-schema": "数据键设置模式", - "edit": "编辑部件", - "editor": "部件编辑器", - "export": "导出部件", - "html": "HTML", - "javascript": "Javascript", - "js": "JS", - "latest": "最新值", - "management": "管理部件", - "missing-widget-title-error": "部件标题必须指定!", - "no-data": "小部件上没有要显示的数据", - "no-data-found": "未找到数据", - "remove": "删除部件", - "remove-resource": "删除资源", - "remove-widget-text": "确认后,控件和所有相关数据将变得不可恢复。", - "remove-widget-title": "确定要删除 '{{widgetTitle}}'部件吗?", - "remove-widget-type": "删除部件类型", - "remove-widget-type-text": "确认后,窗口部件类型和所有相关数据将不可恢复。", - "remove-widget-type-title": "确定要删除部件类型 '{{widgetName}}'吗?", - "resource-is-module": "Is module", - "resource-url": "JavaScript/CSS URL", - "resources": "资源", - "rpc": "控件部件", - "run": "运行部件", - "save": "保存部件", - "save-widget-type-as": "部件类型另存为", - "save-widget-type-as-text": "请输入新的部件标题或选择目标部件包", - "saveAs": "部件另存为", - "search-data": "查找数据", - "select-widget-type": "选择窗口部件类型", - "select-widgets-bundle": "选择部件包", - "settings-schema": "设置模式", - "static": "静态部件", - "tidy": "Tidy", - "timeseries": "Timeseries", - "title": "部件标题", - "title-required": "部件标题必填。", - "toggle-fullscreen": "切换全屏", - "type": "部件类型", - "unable-to-save-widget-error": "无法保存部件!控件有错误!", - "undo": "撤消部件更改", - "widget-bundle": "部件包", - "widget-library": "部件库", - "widget-saved": "部件已保存", - "widget-template-load-failed-error": "无法加载部件模板!", - "widget-type-load-error": "由于以下错误未加载小部件:", - "widget-type-load-failed-error": "无法加载部件类型!", - "widget-type-not-found": "加载部件配置出错。
可能关联的部件已经删除了。" - }, - "widgets-bundle": { - "add": "添加部件包", - "add-widgets-bundle-text": "添加新的部件包", - "create-new-widgets-bundle": "创建新的部件包", - "current": "当前组", - "delete": "删除部件包", - "delete-widgets-bundle-text": "请注意:确认后,部件包和所有相关数据将不可恢复。", - "delete-widgets-bundle-title": "确定要删除部件包 '{{widgetsBundleTitle}}'吗?", - "delete-widgets-bundles-action-title": "删除 { count, plural, 1 {# 个部件包} other {# 个部件包} }", - "delete-widgets-bundles-text": "请注意:确认后,所有选定的部件包将被删除,所有相关数据将不可恢复。", - "delete-widgets-bundles-title": "确定要删除 { count, plural, 1 {# 个部件包} other {# 个部件包} } 吗?", - "description": "描述", - "details": "详情", - "empty": "部件包是空的", - "export": "导出部件包", - "export-failed-error": "无法导出部件包: {{error}}", - "image-preview": "图片预览", - "import": "导入部件包", - "invalid-widgets-bundle-file-error": "无法导入部件包:无效的部件包数据结构。", - "loading-widgets-bundles": "加载部件包...", - "no-widgets-bundles-matching": "未找到与 '{{widgetsBundle}}' 匹配的部件包。", - "no-widgets-bundles-text": "未找到部件包", - "open-widgets-bundle": "打开部件包", - "search": "查找部件包", - "selected-widgets-bundles": "已选择 { count, plural, 1 {# 个部件包} other {# 个部件包} }", - "system": "系统", - "title": "标题", - "title-required": "标题必填。", - "widgets-bundle-details": "部件包详细信息", - "widgets-bundle-file": "部件包文件", - "widgets-bundle-required": "部件包必填。", - "widgets-bundles": "部件包" - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "2 weeks": "2周", - "3 months": "3个月", - "6 months": "6个月", - "Apr": "4月", - "April": "四月", - "Aug": "8月", - "August": "八月", - "Custom Date Range": "自定义日期范围", - "Custom interval": "自定义间隔", - "Date Range Template": "日期范围模板", - "Date picker": "日期选择器", - "Day": "天", - "Dec": "12月", - "December": "十二月", - "Feb": "2月", - "February": "二月", - "Fri": "周五", - "Hour": "小时", - "Interval": "间隔", - "Jan": "1月", - "January": "一月", - "Jul": "7月", - "July": "七月", - "Jun": "6月", - "June": "六月", - "Last Month": "上个月", - "Last Week": "上个星期", - "Last Year": "去年", - "Mar": "3月", - "March": "三月", - "May": "5月", - "Mon": "周一", - "Month": "月", - "Nov": "11月", - "November": "十一月", - "Oct": "10月", - "October": "十月", - "Ok": "Ok", - "Sat": "周六", - "Sep": "9月", - "September": "九月", - "Step size": "步长", - "Sun": "周日", - "This Month": "这个月", - "This Week": "本星期", - "This Year": "今年", - "Thu": "周四", - "Today": "今天", - "Tue": "周二", - "Wed": "周三", - "Week": "周", - "Year": "年", - "Yesterday": "昨天" - } - }, - "input-widgets": { - "attribute-not-allowed": "属性参数不能在此小部件中使用", - "blocked-location": "在浏览器中阻止地理位置", - "claim-device": "声明设备", - "claim-failed": "声明设备失败!", - "claim-not-found": "未找到设备!", - "claim-successful": "设备已成功申领!", - "date": "日期", - "device-name": "设备名称", - "device-name-required": "设备名称必填", - "discard-changes": "放弃更改", - "enable-https-use-widget": "请启用HTTPS以使用此部件", - "entity-attribute-required": "实体属性必填", - "entity-coordinate-required": "纬度和经度两个字段都是必需的", - "entity-timeseries-required": "实体 Timeseries 必填", - "get-location": "获取当前位置", - "invalid-date": "无效日期", - "latitude": "纬度", - "longitude": "经度", - "max-value-error": "最大值是{{value}}", - "min-value-error": "最小值是{{value}}", - "no-attribute-selected": "未选择任何属性", - "no-coordinate-specified": "未指定纬度/经度的数据键", - "no-datakey-selected": "未选择数据键", - "no-entity-selected": "未选择实体", - "no-found-your-camera": "未找到摄像机", - "no-image": "没有图像", - "no-permission-camera": "权限被用户拒绝/此站点无权使用摄像机", - "no-support-geolocation": "您的浏览器不支持地理定位", - "no-support-web-camera": "您的浏览器不支持摄像头", - "no-timeseries-selected": "未选择 Timeseries", - "not-allowed-entity": "所选实体不能具有共享属性", - "secret-key": "密钥", - "secret-key-required": "密钥必填", - "switch-attribute-value": "切换实体属性值", - "switch-camera": "切换摄像机", - "switch-timeseries-value": "切换实体 Timeseries 值", - "take-photo": "拍照", - "time": "时间", - "timeseries-not-allowed": "Timeseries 参数不能用于此部件", - "update-attribute": "更新 attribute", - "update-failed": "更新失败", - "update-successful": "更新成功", - "update-timeseries": "更新 Timeseries", - "value": "数值" + "sms-messages": "短信信息", + "sms-messages-daily-activity": "每天的短信息", + "sms-messages-monthly-activity": "每小时的短信息", + "successful": "${entityName}成功", + "telemetry": "遥测数据", + "telemetry-persistence": "遥测持久化", + "telemetry-persistence-daily-activity": "每天的遥测持久化", + "telemetry-persistence-hourly-activity": "每小时的遥测持久化", + "telemetry-persistence-monthly-activity": "每月的遥测持久化", + "transport": "传输", + "transport-daily-activity": "每天传输数据量", + "transport-data-points": "传输数据点", + "transport-hourly-activity": "每小时传输数据量", + "transport-messages": "传输消息", + "transport-monthly-activity": "每月传输数据量", + "view-details": "查看详细信息", + "view-statistics": "查看统计信息" + }, + "audit-log": { + "audit": "审计", + "audit-logs": "审计日志", + "timestamp": "时间戳", + "entity-type": "实体类型", + "entity-name": "实体名称", + "user": "用户", + "type": "类型", + "status": "状态", + "details": "详情", + "type-added": "添加", + "type-deleted": "删除", + "type-updated": "更新", + "type-attributes-updated": "更新属性", + "type-attributes-deleted": "删除属性", + "type-rpc-call": "RPC调用", + "type-credentials-updated": "更新凭证", + "type-assigned-to-customer": "分配给客户", + "type-unassigned-from-customer": "未分配给客户", + "type-assigned-to-edge": "分配给Edge", + "type-unassigned-from-edge": "未分配给Edge", + "type-activated": "激活", + "type-suspended": "暂停", + "type-credentials-read": "读取凭证", + "type-attributes-read": "读取属性", + "type-relation-add-or-update": "关联已更新", + "type-relation-delete": "关联已删除", + "type-relations-delete": "删除所有关联", + "type-alarm-ack": "已确认", + "type-alarm-clear": "已清除", + "type-login": "登录", + "type-logout": "注销", + "type-lockout": "锁定", + "status-success": "成功", + "status-failure": "失败", + "audit-log-details": "审计日志详情", + "no-audit-logs-prompt": "找不到日志", + "action-data": "活动数据", + "failure-details": "失败详情", + "search": "查找审计日志", + "clear-search": "清空查找", + "type-assigned-from-tenant": "从租户分配", + "type-assigned-to-tenant": "分配给租户", + "type-provision-success": "设备已预配置", + "type-provision-failure": "设备预配置失败", + "type-timeseries-updated": "遥测数据已更新", + "type-timeseries-deleted": "遥测数据已删除" + }, + "confirm-on-exit": { + "message": "你有未保存的更改。确定要离开此页吗?", + "html-message": "你有未保存的更改。
确定要离开此页面吗?", + "title": "未保存的更改" + }, + "contact": { + "country": "国家", + "city": "城市", + "state": "省", + "postal-code": "邮政编码", + "postal-code-invalid": "只允许数字。", + "address": "地址", + "address2": "地址2", + "phone": "电话", + "email": "电子邮件", + "no-address": "无地址", + "state-max-length": "省份名称长度应小于256", + "phone-max-length": "电话号码长度应小于256", + "city-max-length": "城市名称长度应小于256" + }, + "common": { + "username": "用户名", + "password": "密码", + "enter-username": "输入用户名", + "enter-password": "输入密码", + "enter-search": "输入检索条件", + "created-time": "创建时间", + "loading": "正在加载中...", + "proceed": "继续", + "open-details-page": "打开详情页" + }, + "content-type": { + "json": "Json", + "text": "Text", + "binary": "二进制(Base64)" + }, + "customer": { + "customer": "客户", + "customers": "客户", + "management": "客户管理", + "dashboard": "客户仪表板", + "dashboards": "客户仪表板", + "devices": "客户设备", + "entity-views": "客户实体视图", + "assets": "客户资产", + "public-dashboards": "公开仪表板", + "public-devices": "公开设备", + "public-assets": "公开资产", + "public-edges": "公开Edge", + "public-entity-views": "公开实体视图", + "add": "添加客户", + "delete": "删除此客户", + "manage-customer-users": "管理客户用户", + "manage-customer-devices": "管理客户设备", + "manage-customer-dashboards": "管理客户仪表板", + "manage-public-devices": "管理公开设备", + "manage-public-dashboards": "管理公开仪表板", + "manage-customer-assets": "管理客户资产", + "manage-public-assets": "管理公开资产", + "manage-customer-edges": "管理客户Edge", + "manage-public-edges": "管理公开Edge", + "add-customer-text": "添加新客户", + "no-customers-text": "没有找到客户", + "customer-details": "客户详情", + "delete-customer-title": "你确定要删除客户'{{customerTitle}}'吗?", + "delete-customer-text": "请注意:确认删除后客户数据将无法恢复。", + "delete-customers-title": "你确定要删除{ count, plural, 1 {1个客户} other {#个客户} }吗?", + "delete-customers-action-title": "删除{ count, plural, 1 {1个客户} other {#个客户} }", + "delete-customers-text": "请注意:确认删除后客户数据将无法恢复。", + "manage-users": "管理用户", + "manage-assets": "管理资产", + "manage-devices": "管理设备", + "manage-dashboards": "管理仪表板", + "title": "标题", + "title-required": "标题必填。", + "title-max-length": "标题长度应小于256", + "description": "描述", + "details": "详情", + "events": "事件", + "copyId": "复制客户ID", + "idCopiedMessage": "客户ID已复制到粘贴板", + "select-customer": "选择客户", + "no-customers-matching": "没有找到匹配'{{entity}}'的客户。", + "customer-required": "客户必填", + "select-default-customer": "选择默认的客户", + "default-customer": "默认客户", + "default-customer-required": "需要默认客户才能在租户级别调试仪表板。", + "search": "查找客户", + "selected-customers": "已选择{ count, plural, 1 {1个客户} other {#个客户} }", + "edges": "客户Edge实例", + "manage-edges": "管理Edge" + }, + "datetime": { + "date-from": "开始日期", + "time-from": "开始时间", + "date-to": "结束日期", + "time-to": "结束时间" + }, + "dashboard": { + "dashboard": "仪表板", + "dashboards": "仪表板", + "management": "仪表板管理", + "view-dashboards": "查看仪表板", + "add": "添加仪表板", + "assign-dashboard-to-customer": "将仪表板分配给客户", + "assign-dashboard-to-customer-text": "请选择要分配给客户的仪表板", + "assign-dashboard-to-edge-title": "将仪表板分配给Edge", + "assign-to-customer-text": "请选择客户分配仪表板", + "assign-to-customer": "分配给客户", + "unassign-from-customer": "取消分配客户", + "make-public": "仪表板设为公开", + "make-private": "仪表板设为私有", + "manage-assigned-customers": "管理已分配的客户", + "assigned-customers": "已分配的客户", + "assign-to-customers": "将仪表板分配给客户", + "assign-to-customers-text": "请选择需要分配的客户", + "unassign-from-customers": "客户未分配仪表板", + "unassign-from-customers-text": "请选择取消的客户", + "no-dashboards-text": "没有找到仪表板", + "no-widgets": "没有配置部件", + "add-widget": "添加新的部件", + "title": "标题", + "image": "缩略图", + "mobile-app-settings": "移动端设置", + "mobile-order": "移动端显示顺序", + "mobile-hide": "移动端隐藏", + "update-image": "更新缩略图", + "take-screenshot": "截图", + "select-widget-title": "选择部件", + "select-widget-value": "{{title}}: 选择部件", + "select-widget-subtitle": "可用的部件类型列表", + "delete": "删除仪表板", + "title-required": "标题必填。", + "title-max-length": "标题长度应小于256", + "description": "描述", + "details": "详情", + "dashboard-details": "仪表板详情", + "add-dashboard-text": "添加新的仪表板", + "assign-dashboards": "分配仪表板", + "assign-new-dashboard": "分配新的仪表板", + "assign-dashboards-text": "分配{ count, plural, 1 {1个仪表板} other {#个仪表板} }给客户", + "unassign-dashboards-action-text": "取消分配{ count, plural, 1 {1个仪表板} other {#个仪表板} }给客户", + "delete-dashboards": "删除仪表板", + "unassign-dashboards": "取消分配仪表板", + "unassign-dashboards-action-title": "取消分配此客户{ count, plural, 1 {1个仪表板} other {#个仪表板} }", + "delete-dashboard-title": "你确定要删除仪表板'{{dashboardTitle}}'吗?", + "delete-dashboard-text": "请注意:确认删除后仪表板数据将无法恢复。", + "delete-dashboards-title": "确定要删除{ count, plural, 1 {1个仪表板} other {#个仪表板} }吗?", + "delete-dashboards-action-title": "删除{ count, plural, 1 {1个仪表板} other {#个仪表板} }", + "delete-dashboards-text": "请注意:确认删除后仪表板数据将无法恢复。", + "unassign-dashboard-title": "你确定要取消分配仪表板'{{dashboardTitle}}'吗?", + "unassign-dashboard-text": "确认后仪表板将取消分配,客户将无法访问。", + "unassign-dashboard": "取消分配仪表板", + "unassign-dashboards-title": "确定要取消分配仪表板{ count, plural, 1 {1个仪表板} other {#个仪表板} }吗?", + "unassign-dashboards-text": "确认后仪表板将取消分配,客户将无法访问。", + "public-dashboard-title": "仪表板已公开", + "public-dashboard-text": "你的仪表板{{dashboardTitle}} 已被公开,可通过如下链接访问:", + "public-dashboard-notice": "提示:不要忘记将相关设备公开以访问其数据。", + "make-private-dashboard-title": "你确定要将仪表板'{{dashboardTitle}}'设为私有吗?", + "make-private-dashboard-text": "确认后仪表板将设为私有并不允许访问。", + "make-private-dashboard": "仪表板设为私有", + "socialshare-text": "'{{dashboardTitle}}'由Thingsboard提供支持", + "socialshare-title": "'{{dashboardTitle}}'由Thingsboard提供支持", + "select-dashboard": "选择仪表板", + "no-dashboards-matching": "找不到符合'{{entity}}'的仪表板。", + "dashboard-required": "仪表板必填。", + "select-existing": "选择现有仪表板", + "create-new": "创建新的仪表板", + "new-dashboard-title": "新仪表板标题", + "open-dashboard": "打开仪表板", + "set-background": "设置背景", + "background-color": "背景颜色", + "background-image": "背景图片", + "background-size-mode": "背景大小模式", + "no-image": "没有图片", + "empty-image": "没有图片", + "drop-image": "拖拽图像或", + "maximum-upload-file-size": "最大上传文件大小: {{ size }}", + "cannot-upload-file": "无法上传文件", + "settings": "设置", + "layout-settings": "布局设置", + "columns-count": "列数", + "columns-count-required": "需要列数。", + "min-columns-count-message": "只允许最少10列", + "max-columns-count-message": "只允许最多1000列", + "widgets-margins": "部件间边距", + "margin-required": "边距值必填。", + "min-margin-message": "最小边距值为0。", + "max-margin-message": "最大边距值为50。", + "horizontal-margin": "水平边距", + "horizontal-margin-required": "水平边距必填。", + "min-horizontal-margin-message": "最小水平边距值为0。", + "max-horizontal-margin-message": "最大水平边距值为50。", + "vertical-margin": "垂直边距", + "vertical-margin-required": "需要垂直边距值。", + "min-vertical-margin-message": "最小垂直边距值为0。", + "max-vertical-margin-message": "最大垂直边距值为50。", + "autofill-height": "自动填充高度", + "mobile-layout": "移动端布局设置", + "mobile-row-height": "移动端行高(像素)", + "mobile-row-height-required": "移动端行高必填。", + "min-mobile-row-height-message": "移动端行高最小为5像素。", + "max-mobile-row-height-message": "移动端行高最大为200像素。", + "title-settings": "标题设置", + "display-title": "显示标题", + "title-color": "标题颜色", + "toolbar-settings": "工具栏设置", + "hide-toolbar": "隐藏工具栏", + "toolbar-always-open": "始终打开工具栏", + "display-dashboards-selection": "显示仪表板选项", + "display-entities-selection": "显示实体选项", + "display-filters": "显示过滤器", + "display-dashboard-timewindow": "显示时间窗口", + "display-dashboard-export": "显示导出", + "display-update-dashboard-image": "显示仪表板缩略图", + "dashboard-logo-settings": "Logo设置", + "display-dashboard-logo": "全屏模式下显示Logo", + "dashboard-logo-image": "Logo图片", + "advanced-settings": "高级设置", + "dashboard-css": "仪表板样式", + "import": "导入仪表板", + "export": "导出仪表板", + "export-failed-error": "无法导出仪表板: {{error}}", + "create-new-dashboard": "创建新的仪表板", + "dashboard-file": "仪表板文件", + "invalid-dashboard-file-error": "仪表板数据结构无效导入失败。", + "dashboard-import-missing-aliases-title": "配置导入仪表板使用的别名", + "create-new-widget": "添加部件", + "import-widget": "导入部件", + "widget-file": "部件文件", + "invalid-widget-file-error": "部件数据结构无效导入失败。", + "widget-import-missing-aliases-title": "配置导入的窗口部件使用的别名", + "open-toolbar": "打开工具栏", + "close-toolbar": "关闭工具栏", + "configuration-error": "配置错误", + "alias-resolution-error-title": "仪表板别名配置错误", + "invalid-aliases-config": "无法找到与别名和过滤器匹配的设备
请联系你的管理员解决此问题。", + "select-devices": "选择设备", + "assignedToCustomer": "分配给客户", + "assignedToCustomers": "分配给客户", + "public": "公开", + "copyId": "复制仪表板Id", + "idCopiedMessage": "仪表板ID已经复制到粘贴板", + "public-link": "公开链接", + "copy-public-link": "复制公开链接", + "public-link-copied-message": "仪表板的公开链接已被复制到剪贴板", + "manage-states": "仪表板状态管理", + "states": "仪表板状态", + "search-states": "仪表板状态检索", + "selected-states": "已选择{ count, plural, 1 {1个仪表板状态} other {#个仪表板状态} }", + "edit-state": "仪表板状态编辑", + "delete-state": "删除仪表板状态", + "add-state": "添加仪表板状态", + "no-states-text": "找不到状态", + "state": "仪表板状态", + "state-name": "名称", + "state-name-required": "仪表板状态名必填。", + "state-id": "状态ID", + "state-id-required": "仪表板状态ID必填。", + "state-id-exists": "仪表板状态ID已经存在。", + "is-root-state": "根状态", + "delete-state-title": "删除仪表板状态", + "delete-state-text": "确定要删除仪表板状态 '{{stateName}}' 吗?", + "show-details": "显示详情", + "hide-details": "隐藏详情", + "select-state": "选择目标状态", + "state-controller": "状态控制", + "search": "查找仪表板", + "selected-dashboards": "已选择 { count, plural, 1 {1个仪表板} other {#个仪表板} }", + "home-dashboard": "首页仪表板", + "home-dashboard-hide-toolbar": "隐藏首页仪表板工具栏", + "unassign-dashboard-from-edge-text": "确认后仪表板将取消分配,Edge将无法访问。", + "unassign-dashboards-from-edge-title": "确定要取消分配仪表板{ count, plural, 1 {1个仪表板} other {#个仪表板} }吗?", + "unassign-dashboards-from-edge-text": "确认后仪表板将取消分配,Edge将无法访问。", + "assign-dashboard-to-edge": "将仪表板分配给Edge", + "assign-dashboard-to-edge-text": "请选择要分配给Edge的仪表板", + "non-existent-dashboard-state-error": "仪表盘\"{{ stateId }}\"未找到" + }, + "datakey": { + "settings": "设置", + "advanced": "高级", + "label": "标签", + "color": "颜色", + "units": "单位符号", + "decimals": "小数位数", + "data-generation-func": "数据生成函数", + "use-data-post-processing-func": "数据处理函数", + "configuration": "数据键配置", + "timeseries": "时间序列", + "attributes": "属性", + "entity-field": "实体字段", + "alarm": "报警字段", + "timeseries-required": "时间序列必填。", + "timeseries-or-attributes-required": "时间序列/属性必填。", + "alarm-fields-timeseries-or-attributes-required": "警报字段或时间序列/属性必填。", + "maximum-timeseries-or-attributes": "最多允许{ count, plural, 1 {1个时间序列/属性。} other {#个时间序列/属性。}}", + "alarm-fields-required": "警报字段必填。", + "function-types": "函数类型", + "function-type": "函数类型", + "function-types-required": "函数类型必填。", + "alarm-keys": "报警数据键", + "alarm-key": "报警数据键", + "alarm-key-functions": "报警数据函数", + "alarm-key-function": "报警数据函数", + "latest-keys": "最新数据键", + "latest-key": "最新数据键", + "latest-key-functions": "最新数据函数", + "latest-key-function": "最新数据函数", + "timeseries-keys": "时序数据键", + "timeseries-key": "时序数据键", + "timeseries-key-functions": "时序数据函数", + "timeseries-key-function": "时序数据函数", + "maximum-function-types": "最多允许{ count, plural, 1 {1个函数类型} other {#个函数类型} }", + "time-description": "当前值的时间戳;", + "value-description": "当前值;", + "prev-value-description": "上一次函数调用的结果;", + "time-prev-description": "上一个值的时间戳;", + "prev-orig-value-description": "之前的原始值;", + "aggregation": "聚合", + "aggregation-type-hint-common": "出于性能原因聚合值计算仅适用于“当前日期”、“当前月份”等固定时间间隔,而不适用于“最近30分钟”或“最近24小时”等滑动窗口间隔。", + "aggregation-type-none-hint": "取最新值。", + "aggregation-type-min-hint": "在选定时间窗口内的数据点中查找最小值。", + "aggregation-type-max-hint": "在选定时间窗口内的数据点中查找最大值。", + "aggregation-type-avg-hint": "计算选定时间窗口内数据点之间的平均值。", + "aggregation-type-sum-hint": "对选定时间窗口内所有数据点的值求和。", + "aggregation-type-count-hint": "选定时间窗口内的数据点总数。", + "delta-calculation": "增量计算", + "enable-delta-calculation": "启用增量计算", + "enable-delta-calculation-hint": "启用后将根据选定时间窗口和指定比较时段的聚合值计算数据键值。出于性能原因,增量计算仅适用于历史时间窗口,而不适用于实时值。例如,您可以计算昨天的能耗与前天的能耗之间的差值。", + "delta-calculation-result": "增量计算结果", + "delta-calculation-result-previous-value": "上个值", + "delta-calculation-result-delta-absolute": "绝对值", + "delta-calculation-result-delta-percent": "百分比" + }, + "datasource": { + "type": "数据源类型", + "name": "名称", + "label": "标签", + "add-datasource-prompt": "请添加数据源" + }, + "details": { + "details": "详情", + "edit-mode": "编辑模式", + "edit-json": "编辑JSON", + "toggle-edit-mode": "切换编辑模式" + }, + "device": { + "device": "设备", + "device-required": "设备必填", + "devices": "设备", + "management": "设备管理", + "view-devices": "查看设备", + "device-alias": "设备别名", + "device-type-max-length": "设备类型长度应小于256", + "aliases": "设备别名", + "no-alias-matching": "'{{alias}}'没有找到。", + "no-aliases-found": "找不到别名。", + "no-key-matching": "'{{key}}'没有找到。", + "no-keys-found": "找不到密钥。", + "create-new-alias": "创建新的别名!", + "create-new-key": "创建新的键!", + "duplicate-alias-error": "找到重复别名'{{alias}}'。
设备别名必须是唯一的。", + "configure-alias": "配置'{{alias}}'别名", + "no-devices-matching": "找不到与'{{entity}}'匹配的设备。", + "alias": "别名", + "alias-required": "设备别名必填。", + "remove-alias": "删除设备别名", + "add-alias": "添加设备别名", + "name-starts-with": "名称前缀", + "help-text": "根据需要可以使用'%'进行匹配,例如:'%device_name_contains%','%device_name_ends','device_starts_with'。", + "device-list": "设备列表", + "use-device-name-filter": "使用过滤器", + "device-list-empty": "没有被选中的设备", + "device-name-filter-required": "设备名称过滤器必填。", + "device-name-filter-no-device-matched": "找不到以'{{device}}'开头的设备。", + "add": "添加设备", + "assign-to-customer": "分配给客户", + "assign-device-to-customer": "将设备分配给客户", + "assign-device-to-customer-text": "请选择要分配给客户的设备", + "assign-device-to-edge-title": "将设备分配给Edge", + "assign-device-to-edge-text": "请选择要分配给Edge的设备", + "make-public": "公开", + "make-private": "私有", + "no-devices-text": "找不到设备", + "assign-to-customer-text": "请选择客户分配设备", + "device-details": "设备详细信息", + "add-device-text": "添加新设备", + "credentials": "凭据", + "manage-credentials": "管理凭据", + "delete": "删除设备", + "assign-devices": "分配设备", + "assign-devices-text": "将{count,plural,1 {#个设备} other {#个设备} }分配给客户", + "delete-devices": "删除设备", + "unassign-from-customer": "取消分配客户", + "unassign-devices": "取消分配设备", + "unassign-devices-action-title": "取消分配此客户的{count,plural,1 {#个设备} other {#个设备} }", + "unassign-device-from-edge-title": "你确定要取消设备'{{deviceName}}'的分配吗?", + "unassign-device-from-edge-text": "确认后分配将被取消,Edge将无法访问。", + "unassign-devices-from-edge": "取消分配Edge", + "assign-new-device": "分配新设备", + "make-public-device-title": "你确定要将设备'{{deviceName}}'设为公开吗?", + "make-public-device-text": "确认后设备数据将公开并允许访问。", + "make-private-device-title": "你确定要将设备'{{deviceName}}'设为私有吗?", + "make-private-device-text": "确认后设备数据私有并不并允许访问。", + "view-credentials": "查看凭证", + "delete-device-title": "你确定要删除设备的{{deviceName}}吗?", + "delete-device-text": "请注意:确认删除后设备关数据将无法恢复。", + "delete-devices-title": "确定要删除{count,plural,1 {#个设备} other {#个设备} }吗?", + "delete-devices-action-title": "删除{count,plural,1 {#个设备} other {#个设备} }", + "delete-devices-text": "请注意:确认删除后设备数据将无法恢复。", + "unassign-device-title": "你确定要取消'{{deviceName}}'设备的分配吗?", + "unassign-device-text": "确认后设备分配将被取消,客户将无法访问。", + "unassign-device": "取消分配设备", + "unassign-devices-title": "确定要取消分配 {count,plural,1 {#个设备} other {#个设备} } 吗?", + "unassign-devices-text": "确认后设备分配将取消,客户将无法访问。", + "device-credentials": "设备凭据", + "loading-device-credentials": "加载设备凭证...", + "credentials-type": "凭据类型", + "access-token": "访问令牌", + "access-token-required": "访问令牌必填", + "access-token-invalid": "访问令牌长度必须为1到32个字符。", + "certificate-pem-format": "PEM证书", + "certificate-pem-format-required": "PEM证书必填。", + "lwm2m-security-config": { + "identity": "客户端标识", + "identity-required": "客户端标识必填。", + "identity-tooltip": "PSK标识符是多达128字节的任意PSK标识符,如标准[RFC7925]所述。", + "client-key": "客户端密钥", + "client-key-required": "客户端密钥必填。", + "client-key-tooltip-prk": "RPK公钥必须满足标准[RFC7250]并且Base64编码!", + "client-key-tooltip-psk": "PSK密钥必须是[RFC4279]标准和HexDec格式: 32, 64, 128字符!", + "endpoint": "客户端名称", + "endpoint-required": "客户端名称必填。", + "client-public-key": "客户端公钥", + "client-public-key-hint": "如果客户端公钥为空则使用信任证书", + "client-public-key-tooltip": "X509公钥必须是DER-encoded X509v3格式并支持EC算法编码Base64!", + "mode": "安全配置模式", + "client-tab": "客户端安全配置", + "client-certificate": "客户端证书", + "bootstrap-tab": "Bootstrap Client", + "bootstrap-server": "Bootstrap Server", + "lwm2m-server": "LwM2M Server", + "client-publicKey-or-id": "客户端公钥或ID", + "client-publicKey-or-id-required": "客户端公钥或ID必填。", + "client-publicKey-or-id-tooltip-psk": "SK标识符是最多128字节,如标准[RFC7925]所述。", + "client-publicKey-or-id-tooltip-rpk": "RPK必须满足标准[RFC7250]而且编码为 Base64!", + "client-publicKey-or-id-tooltip-x509": "X509公钥必须是X509v3格式而且支持EC算法", + "client-secret-key": "客户端密钥", + "client-secret-key-required": "客户端密钥必填。", + "client-secret-key-tooltip-psk": "PSK键必须是[RFC4279]标准和HexDec格式: 32, 64, 128i字符!", + "client-secret-key-tooltip-prk": "RPK密钥必须是PKCS_8格式(DER编码, 标准[RFC5958]) 并编码为Base64!", + "client-secret-key-tooltip-x509": "X509密钥必须是PKCS_8格式(DER编码, 标准[RFC5958])!" + }, + "client-id": "客户端ID", + "client-id-pattern": "客户端ID包含无效字符。", + "user-name": "用户名", + "user-name-required": "用户名必填。", + "client-id-or-user-name-necessary": "客户端ID和用户名是必填。", + "password": "密码", + "secret": "密钥", + "secret-required": "密钥必填", + "device-type": "设备类型", + "device-type-required": "设备类型必填。", + "select-device-type": "选择设备类型", + "enter-device-type": "输入设备类型", + "any-device": "任意设备", + "no-device-types-matching": "没有找到符合'{{entitySubtype}}'的设备类型。", + "device-type-list-empty": "未选择设备类型", + "device-types": "设备类型", + "name": "名称", + "name-required": "名称必填。", + "name-max-length": "名称长度不能大于256", + "label-max-length": "标签长度应小于256", + "description": "描述", + "label": "标签", + "events": "事件", + "details": "详情", + "copyId": "复制设备ID", + "copyAccessToken": "复制访问令牌", + "copy-mqtt-authentication": "复制MQTT凭据", + "idCopiedMessage": "设备ID已复制到剪贴板", + "accessTokenCopiedMessage": "设备访问令牌已复制到剪贴板", + "mqtt-authentication-copied-message": "设备MQTT身份验证已复制到剪贴板", + "assignedToCustomer": "分配给客户", + "unable-delete-device-alias-title": "无法删除设备别名", + "unable-delete-device-alias-text": "设备别名'{{deviceAlias}}'不能够被删除,因为它被下列部件正在使用:
{{widgetsList}}", + "is-gateway": "是否网关", + "overwrite-activity-time": "覆盖已连接设备的活动时间", + "public": "公开", + "device-public": "设备公开", + "select-device": "选择设备", + "import": "导入设备", + "device-file": "设备文件", + "search": "查找设备", + "selected-devices": "已选择{ count, plural, 1 {1个设备} other {#个设备} }", + "device-configuration": "设备配置", + "transport-configuration": "传输配置", + "wizard": { + "device-wizard": "设备向导", + "device-details": "设备详细信息", + "new-device-profile": "新建设备配置", + "existing-device-profile": "选择已有设备配置", + "specific-configuration": "指定配置", + "customer-to-assign-device": "客户分配设备", + "add-credentials": "添加凭据" + }, + "unassign-devices-from-edge-title": "你确定要取消分配{ count, plural, 1 {1个设备} other {#个设备} }吗?", + "unassign-devices-from-edge-text": "确认后设备将被取消分配,Edge将无法访问。" + }, + "asset-profile": { + "asset-profile": "资产配置", + "asset-profiles": "资产配置", + "all-asset-profiles": "全部", + "add": "添加资产配置", + "edit": "编辑资产配置", + "asset-profile-details": "资产配置详情", + "no-asset-profiles-text": "找不到资产配置", + "search": "查找资产配置", + "selected-asset-profiles": "已选择{ count, plural, 1 {1个资产配置} other {#个资产配置} }", + "no-asset-profiles-matching": "找不到与'{{entity}}'匹配的资产配置。", + "asset-profile-required": "资产配置必填", + "idCopiedMessage": "资产配置ID已复制到剪贴板", + "set-default": "设为默认资产配置", + "delete": "删除资产配置", + "copyId": "复制资产配置ID", + "name-max-length": "名称长度不能大于256", + "new-device-profile-name": "资产配置名称", + "new-device-profile-name-required": "资产配置名称必填。", + "name": "名称", + "name-required": "名称是必填的。", + "image": "资产配置图片", + "description": "描述", + "default": "默认", + "default-rule-chain": "默认规则链", + "mobile-dashboard": "移动端仪表板", + "mobile-dashboard-hint": "移动应用程序用作资产详细信息仪表板", + "select-queue-hint": "从下拉列表中选择。", + "delete-asset-profile-title": "确定要删除'{{assetProfileName}}资产配置'?", + "delete-asset-profile-text": "请注意:确认删除后资产配置数据将无法恢复。", + "delete-asset-profiles-title": "确定要删除{ count, plural, 1 {1资产配置} other {#资产配置} }?", + "delete-asset-profiles-text": "请注意:确认删除后资产配置数据将无法恢复。", + "set-default-asset-profile-title": "确实将'{{assetProfileName}}'资产配置设为默认吗?", + "set-default-asset-profile-text": "确认后资产配置将被标记为默认,并将默认指定给新资产。", + "no-asset-profiles-found": "找不到资产配置。", + "create-new-asset-profile": "创建新的资产配置!", + "create-asset-profile": "创建新的资产配置", + "import": "导入资产配置", + "export": "导出资产配置", + "export-failed-error": "无法导出资产配置文件: {{error}}", + "asset-profile-file": "资产配置文件", + "invalid-asset-profile-file-error": "资产配置数据结构无效导入失败。" + }, + "device-profile": { + "device-profile": "设备配置", + "device-profiles": "设备配置", + "all-device-profiles": "全部", + "add": "添加设备配置", + "edit": "编辑设备配置", + "device-profile-details": "设备配置详情", + "no-device-profiles-text": "找不到设备配置", + "search": "查找设备配置", + "selected-device-profiles": "已选择{ count, plural, 1 {1个设备配置} other {#个设备配置} }", + "no-device-profiles-matching": "找不到与'{{entity}}'匹配的设备配置。", + "device-profile-required": "设备配置必填", + "idCopiedMessage": "设备配置ID已复制到剪贴板", + "set-default": "设为默认设备配置", + "delete": "删除设备配置", + "copyId": "复制设备配置ID", + "name-max-length": "名称长度不能大于256", + "new-device-profile-name": "设备配置名称", + "new-device-profile-name-required": "设备配置名称必填。", + "name": "名称", + "name-required": "名称是必填的。", + "type": "配置类型", + "type-required": "配置类型必填。", + "type-default": "默认", + "image": "设备配置图片", + "transport-type": "传输方式", + "transport-type-required": "传输方式必填。", + "transport-type-default": "默认", + "transport-type-default-hint": "支持基本MQTT、HTTP和CoAP传输", + "transport-type-mqtt": "MQTT", + "transport-type-mqtt-hint": "启用高级MQTT传输设置", + "transport-type-coap": "CoAP", + "transport-type-coap-hint": "启用高级CoAP传输设置", + "transport-type-lwm2m": "LWM2M", + "transport-type-lwm2m-hint": "LWM2M传输类型", + "transport-type-snmp": "SNMP", + "transport-type-snmp-hint": "指定SNMP传输配置", + "description": "描述", + "default": "默认", + "profile-configuration": "配置", + "transport-configuration": "传输配置", + "default-rule-chain": "默认规则链", + "mobile-dashboard": "移动端仪表板", + "mobile-dashboard-hint": "移动应用程序用作设备详细信息仪表板", + "select-queue-hint": "从下拉列表中选择。", + "delete-device-profile-title": "确定要删除'{{deviceProfileName}}'设备配置?", + "delete-device-profile-text": "请注意:确认删除后设备配置数据将无法恢复。", + "delete-device-profiles-title": "确定要删除{ count, plural, 1 {1个设备配置} other {#个设备配置} }?", + "delete-device-profiles-text": "请注意:确认删除后设备配置数据将无法恢复。", + "set-default-device-profile-title": "确实将'{{deviceProfileName}}'设备配置设为默认吗?", + "set-default-device-profile-text": "确认后设备配置将被标记为默认,并将默认指定给新设备。", + "no-device-profiles-found": "找不到设备配置。", + "create-new-device-profile": "创建新的设备配置!", + "mqtt-device-topic-filters": "MQTT设备Topic过滤器", + "mqtt-device-topic-filters-unique": "MQTT设备Topic过滤器必须唯一。", + "mqtt-device-payload-type": "MQTT设备Payload", + "mqtt-device-payload-type-json": "JSON", + "mqtt-device-payload-type-proto": "Protobuf", + "mqtt-enable-compatibility-with-json-payload-format": "启用与其他payload格式兼容。", + "mqtt-enable-compatibility-with-json-payload-format-hint": "启用后平台将默认使用Protobuf的payload格式,如果解析失败平台将尝试使用JSON的payload格式。对于固件更新期间的向后兼容性很有用,例如固件的初始版本使用Json而新版本使用Protobuf在设备队列的固件更新过程中,需要同时支持Protobuf和JSON。兼容模式会导致一点的性能下降,因此建议在所有设备更新后禁用此模式。", + "mqtt-use-json-format-for-default-downlink-topics": "缺省下行主题采用json格式", + "mqtt-use-json-format-for-default-downlink-topics-hint": "启用后平台将使用Json的playload格式通过以下主题推送属性和RPC:v1/devices/me/attributes/response/$request_id、v1/devices/me/attributes 、v1/devices/me/rpc/request/$request_id、v1/devices/me/rpc/response/$request_id。此设置不会影响使用新(v2)主题发送的属性和rpc订阅:v2/a/res/$request_id、v2/a、v2/r /req/$request_id,v2/r/res/$request_id。其中$request_id是一个整数请求标识符。", + "mqtt-send-ack-on-validation-exception": "发布消息验证失败时发送PUBACK", + "mqtt-send-ack-on-validation-exception-hint": "默认情况下平台将关闭相关消息验证失败的MQTT会话,启用后平台将发布确认而不是关闭会话。", + "snmp-add-mapping": "添加SNMP映射", + "snmp-mapping-not-configured": "没有配置时间序列/属性的OID映射", + "snmp-timseries-or-attribute-name": "映射时间序列/属性名称", + "snmp-timseries-or-attribute-type": "映射时间序列/属性名称", + "snmp-method-pdu-type-get-request": "GetRequest", + "snmp-method-pdu-type-get-next-request": "GetNextRequest", + "snmp-oid": "OID", + "transport-device-payload-type-json": "JSON", + "transport-device-payload-type-proto": "Protobuf", + "mqtt-payload-type-required": "Payload类型必填。", + "coap-device-type": "CoAP设备类型", + "coap-device-payload-type": "CoAP设备消息Payload", + "coap-device-type-required": "CoAP设备类型必填。", + "coap-device-type-default": "默认", + "coap-device-type-efento": "Efento NB-IoT", + "support-level-wildcards": "支持单[+]和多级[#]通配符。", + "telemetry-topic-filter": "遥测数据topic过滤器", + "telemetry-topic-filter-required": "遥测数据topic过滤器必填。", + "attributes-topic-filter": "属性主题过滤器", + "attributes-topic-filter-required": "属性主题过滤器必填。", + "telemetry-proto-schema": "遥测数据proto schema", + "telemetry-proto-schema-required": "遥测数据proto schema必填。", + "attributes-proto-schema": "属性proto schema", + "attributes-proto-schema-required": "属性proto schema必填。", + "rpc-response-proto-schema": "RPC响应proto schema", + "rpc-response-proto-schema-required": "RPC响应proto schema必填。", + "rpc-response-topic-filter": "RPC响应Topic过滤器", + "rpc-response-topic-filter-required": "RPC响应Topic过滤器必填。", + "rpc-request-proto-schema": "RPC请求proto schema", + "rpc-request-proto-schema-required": "RPC请求proto schema必填。", + "rpc-request-proto-schema-hint": "RPC请求消息应始终包含字段:string method = 1; int32 requestId = 2; 和params = 3的任何数据类型。", + "not-valid-pattern-topic-filter": "无效的Topic过滤器模式", + "not-valid-single-character": "单级通配符的使用无效", + "not-valid-multi-character": "多级通配符的使用无效", + "single-level-wildcards-hint": "[+] 适用于任何主题过滤,例如:v1/devices/+/telemetry或+/devices/+/attributes。", + "multi-level-wildcards-hint": "[#]可以替换topic本身,并且必须是topic的最后一个符号,例如:#或v1/devices/me/#。", + "alarm-rules": "报警规则", + "alarm-rules-with-count": "报警规则({{count}})", + "no-alarm-rules": "未配置报警规则", + "add-alarm-rule": "添加报警规则", + "edit-alarm-rule": "编辑报警规则", + "alarm-type": "报警类型", + "alarm-type-required": "报警类型必填。", + "alarm-type-unique": "警报类型在设备配置警报规则中必须唯一。", + "alarm-type-max-length": "警报类型长度必须少于256个字符", + "create-alarm-pattern": "创建{{alarmType}}报警", + "create-alarm-rules": "创建报警规则", + "no-create-alarm-rules": "未配置创建条件", + "add-create-alarm-rule-prompt": "请添加创建报警规则", + "clear-alarm-rule": "清除报警规则", + "no-clear-alarm-rule": "未配置明确条件", + "add-create-alarm-rule": "添加创建条件", + "add-clear-alarm-rule": "添加清除条件", + "select-alarm-severity": "选择报警严重性", + "alarm-severity-required": "报警严重级别必填。", + "condition-duration": "条件持续时间", + "condition-duration-value": "持续时间值", + "condition-duration-time-unit": "时间单位", + "condition-duration-value-range": "持续时间值应在1到2147483647之间。", + "condition-duration-value-pattern": "持续时间值应为整数。", + "condition-duration-value-required": "持续时间值必填。", + "condition-duration-time-unit-required": "时间单位必填。", + "advanced-settings": "高级设置", + "alarm-rule-details": "详情", + "alarm-rule-details-hint": "提示:使用${keyName}警报条件中属性值或报文关键字。", + "add-alarm-rule-details": "详情模板:", + "alarm-rule-mobile-dashboard": "移动仪表盘", + "alarm-rule-mobile-dashboard-hint": "用于警报详细报告", + "alarm-rule-no-mobile-dashboard": "未选择仪表板", + "propagate-alarm": "向相关实体传播警报", + "alarm-rule-relation-types-list": "要传递的关联类型", + "alarm-rule-relation-types-list-hint": "如果未选择传递关联类型,则将不按关联类型过滤而传递报警。", + "propagate-alarm-to-owner": "向实体所有者(客户或租户)传播警报", + "propagate-alarm-to-tenant": "向租户传播警报", + "alarm-details": "报警详细信息", + "alarm-rule-condition": "报警规则条件", + "enter-alarm-rule-condition-prompt": "请添加报警规则条件", + "edit-alarm-rule-condition": "编辑报警规则条件", + "device-provisioning": "设备预配置", + "provision-strategy": "预配置策略", + "provision-strategy-required": "预配置策略必填。", + "provision-strategy-disabled": "禁用", + "provision-strategy-created-new": "允许创建新设备", + "provision-strategy-check-pre-provisioned": "检查预配置的设备", + "provision-device-key": "预配置设备密钥名", + "provision-device-key-required": "预配置设备密钥名必填。", + "copy-provision-key": "复制预配置密钥名", + "provision-key-copied-message": "预配置密钥名已复制到剪贴板", + "provision-device-secret": "预配置设备密钥", + "provision-device-secret-required": "预配置设备密钥必填。", + "copy-provision-secret": "复制预配置密钥", + "provision-secret-copied-message": "预配置密钥已复制到剪贴板", + "condition": "条件", + "condition-type": "条件类型", + "condition-type-simple": "简单", + "condition-type-duration": "持续时间", + "condition-during": "在{{during}}期间", + "condition-during-dynamic": "在\"{{ attribute }}\" ({{during}})", + "condition-type-repeating": "重复", + "condition-type-required": "条件类型必填。", + "condition-repeating-value": "事件计数", + "condition-repeating-value-range": "事件计数应在1到2147483647之间。", + "condition-repeating-value-pattern": "事件计数应为整数。", + "condition-repeating-value-required": "事件计数值必填。", + "condition-repeat-times": "重复{ count, plural, 1 {#次} other {#次} }", + "condition-repeat-times-dynamic": "重复\"{ attribute }\" ({ count, plural, 1 {1时间} other {#时间} })", + "schedule-type": "计划程序类型", + "schedule-type-required": "计划类型必填。", + "schedule": "启用规则:", + "edit-schedule": "编辑报警日程表", + "schedule-any-time": "始终启用", + "schedule-specific-time": "定时启用", + "schedule-custom": "自定义启用", + "schedule-day": { + "monday": "星期一", + "tuesday": "星期二", + "wednesday": "星期三", + "thursday": "星期四", + "friday": "星期五", + "saturday": "星期六", + "sunday": "星期日" + }, + "schedule-days": "天", + "schedule-time": "时间", + "schedule-time-from": "从", + "schedule-time-to": "到", + "schedule-days-of-week-required": "每周至少选择一天。", + "create-device-profile": "创建设备配置", + "import": "导入设备配置", + "export": "导出设备配置", + "export-failed-error": "无法导出设备配置文件: {{error}}", + "device-profile-file": "设备配置文件", + "invalid-device-profile-file-error": "设备配置数据结构无效导入失败。", + "power-saving-mode": "节能模式", + "power-saving-mode-type": { + "default": "使用设备配置的节能模式", + "psm": "节能模式(PSM)", + "drx": "非连续接收(DRX)", + "edrx": "连续接收(eDRX)" + }, + "edrx-cycle": "eDRX循环", + "edrx-cycle-required": "eDRX循环必填。", + "edrx-cycle-pattern": "eDRX循环必须是一个正整数。", + "edrx-cycle-min": "eDRX循环的最小值{{ min }}秒。", + "paging-transmission-window": "分页传输窗口", + "paging-transmission-window-required": "分页传输窗口必填。", + "paging-transmission-window-pattern": "分页传输窗口必须是正整数。", + "paging-transmission-window-min": "分页传输窗口的最小值{{ min }}秒。", + "psm-activity-timer": "PSM活动计时器", + "psm-activity-timer-required": "PSM活动计时器必填。", + "psm-activity-timer-pattern": "PSM活动计时器必须是正整数", + "psm-activity-timer-min": "PSM活动计时器的最小数量为{{ min }}秒。", + "lwm2m": { + "object-list": "Object列表", + "object-list-empty": "没有选择边object。", + "no-objects-found": "没有选择边object。", + "no-objects-matching": "没有找到匹配的object'{{object}}'。", + "model-tab": "LWM2M模式", + "add-new-instances": "添加新实例", + "instances-list": "实例列表", + "instances-list-required": "实例列表必填。", + "instance-id-pattern": "实例id必须是一个正整数。", + "instance-id-max": "实例id最大值是{{max}}", + "instance": "实例", + "resource-label": "#I资源D名称", + "observe-label": "观察者", + "attribute-label": "属性", + "telemetry-label": "遥测", + "edit-observe-select": "选择观察者编辑遥测或属性", + "edit-attributes-select": "选择要编辑的遥测或属性", + "no-attributes-set": "没有设置属性", + "key-name": "键名", + "key-name-required": "键名必填", + "attribute-name": "属性名称", + "attribute-name-required": "属性名称必填。", + "attribute-value": "属性值", + "attribute-value-required": "属性值必填。", + "attribute-value-pattern": "属性值必须是一个正整数。", + "edit-attributes": "编辑属性:{{ name }}", + "view-attributes": "查看属性:{{ name }}", + "add-attribute": "添加属性", + "edit-attribute": "编辑属性", + "view-attribute": "查看属性", + "remove-attribute": "移除属性", + "delete-server-text": "请注意确认删除后服务器配置将无法恢复。", + "delete-server-title": "你确定要删除服务器吗?", + "mode": "安全配置模式", + "bootstrap-tab": "Bootstrap", + "bootstrap-server-legend": "Bootstrap Server (ShortId...)", + "lwm2m-server-legend": "LwM2M Server (ShortId...)", + "server": "服务器", + "short-id": "服务器ID", + "short-id-tooltip": "服务器ID用作关联服务器对象实例的链接。", + "short-id-required": "服务器ID必填。", + "short-id-range": "服务器ID应在1到65534范围内。", + "short-id-pattern": "服务器ID必须是一个正整数。", + "lifetime": "客户端注册生命周期", + "lifetime-required": "客户端注册生命周期必填。", + "lifetime-pattern": "客户端注册生命周期必须是一个正整数。", + "default-min-period": "最小期限", + "default-min-period-tooltip": "LWM2M客户端在观察中不包含此参数时使用的默认值。", + "default-min-period-required": "最小期限必填。", + "default-min-period-pattern": "最小期限必须是一个正整数。", + "notification-storing": "禁用或离线时通知存储", + "binding": "绑定", + "binding-type": { + "u": "U: 客户端通过UDP绑定。", + "m": "M: 客户端通过MQTT绑定。", + "h": "H: 客户端通过HTTP绑定。", + "t": "T: 客户端通过TCP绑定。", + "s": "S: 客户端通过SMS绑定。", + "n": "N: 客户端通过非IP绑定将响应发送到请求(支持LWM2M 1.1)。", + "uq": "UQ: 通过UDP队列模式连接(不支持LWM2M 1.1)。", + "uqs": "UQS: 通过UDP和SMS活动连接(不支持LWM2M 1.1)。", + "tq": "TQ: 通过TCP队列模式连接(不支持LWM2M 1.1)。", + "tqs": "TQS: 通过TCP和SMS活动连接(不支持LWM2M 1.1)。", + "sq": "SQ: 通过队列模式的SMS连接(不支持LWM2M 1.1)。" + }, + "binding-tooltip": "这是LwM2M服务器对象的\"绑定\"资源列表 - /1/x/7。\n表示LwM2M客户端中支持的绑定模式。\n此值应与设备对象(/3/0/16)中的\"支持的绑定和模式\"资源。\n虽然支持多个传输但在整个传输会话期间只能使用一个传输绑定。\n 例如:当UDP和SMS都受支持,LwM2M客户端和LwM2M服务器可以选择在整个传输会话期间通过UDP或SMS进行通信。", + "bootstrap-server": "Bootstrap Server", + "lwm2m-server": "LwM2M Server", + "include-bootstrap-server": "包含Bootstrap Server更新", + "bootstrap-update-title": "你已经配置了Bootstrap Server,您确定要排除更新吗?", + "bootstrap-update-text": "请注意确认更新后Bootstrap Server配置数据将无法恢复。", + "server-host": "主机", + "server-host-required": "主机必填。", + "server-port": "端口", + "server-port-required": "端口必填。", + "server-port-pattern": "端口必须是一个正整数。", + "server-port-range": "端口应在1到65535范围内。", + "server-public-key": "服务器公钥", + "server-public-key-required": "服务器公钥必填。", + "client-hold-off-time": "停留时间", + "client-hold-off-time-required": "停留时间必填。", + "client-hold-off-time-pattern": "停留时间必须是一个正整数。", + "client-hold-off-time-tooltip": "客户端仅与Bootstrap-Server共用停留时间", + "account-after-timeout": "帐户超时", + "account-after-timeout-required": "帐户超时必填。", + "account-after-timeout-pattern": "帐户超时必须是一个正整数。", + "account-after-timeout-tooltip": "Bootstrap-Server帐户资源的超时值。", + "server-type": "Server type", + "add-new-server-title": "添加新的服务器配置", + "add-server-config": "添加服务器配置", + "add-lwm2m-server-config": "添加LwM2M服务器", + "no-config-servers": "没有服务器配置", + "others-tab": "其它设置", + "client-strategy": "客户端连接策略", + "client-strategy-label": "策略", + "client-strategy-only-observe": "只在初始连接后观察对客户的请求", + "client-strategy-read-all": "注册后阅读所有资源并观察对客户的请求", + "fw-update": "固件升级", + "fw-update-strategy": "固件升级策略", + "fw-update-strategy-data": "发布固件升级二制文件使用Object 19和Resource 0数据。", + "fw-update-strategy-package": "发布固件升级二制文件使用Object 5和Resource 0包", + "fw-update-strategy-package-uri": "自动生成唯一的CoAP地址下载包和发布软件更新作为Object 5和Resource 1(软件包URI)。", + "sw-update": "软件更新", + "sw-update-strategy": "软件更新策略", + "sw-update-strategy-package": "发布二制文件使用Object 9和Resource 2(包)", + "sw-update-strategy-package-uri": "自动生成唯一的CoAP地址下载包和发布软件更新作为Object 9和Resource 3(软件包URI)。", + "fw-update-resource": "固件更新COAP资源", + "fw-update-resource-required": "固件更新COAP资源必填。", + "sw-update-resource": "软件更新COAP资源", + "sw-update-resource-required": "软件更新COAP资源必填。", + "config-json-tab": "设备配置JSON", + "attributes-name": { + "min-period": "最小期限", + "max-period": "最大期限", + "greater-than": "大于", + "less-than": "少于", + "step": "停止", + "min-evaluation-period": "最小期限评估", + "max-evaluation-period": "最大期限评估" + }, + "composite-operations-support": "支持Read/Write/Observe操作" + }, + "snmp": { + "add-communication-config": "添加通信配置", + "add-mapping": "添加映射", + "authentication-passphrase": "身份验证密码", + "authentication-passphrase-required": "身份验证密码必填。", + "authentication-protocol": "身份验证协议", + "authentication-protocol-required": "身份验证协议必填。", + "communication-configs": "通信配置", + "community": "Community字符串", + "community-required": "Community字符串必填。", + "context-name": "上下文名称", + "data-key": "数据键", + "data-key-required": "数据键必填。", + "data-type": "数据类型", + "data-type-required": "数据类型必填。", + "engine-id": "引擎ID", + "host": "主机", + "host-required": "主机必填。", + "oid": "OID", + "oid-pattern": "无效OID格式", + "oid-required": "OID必填。", + "please-add-communication-config": "请添加通信配置", + "please-add-mapping-config": "请添加映射配置", + "port": "端口", + "port-format": "端口格式无效", + "port-required": "端口必填。", + "privacy-passphrase": "私有密码", + "privacy-passphrase-required": "私有密码必填。", + "privacy-protocol": "私有协议", + "privacy-protocol-required": "私有协议必填。", + "protocol-version": "协议版本", + "protocol-version-required": "协议版本必填。", + "querying-frequency": "查询频率(ms)", + "querying-frequency-invalid-format": "查询频率必须是一个正整数。", + "querying-frequency-required": "查询频率必填。", + "retries": "重试", + "retries-invalid-format": "重试必须是一个正整数。", + "retries-required": "重试必填。", + "scope": "范围", + "scope-required": "范围必填。", + "security-name": "Security名称", + "security-name-required": "Security名称必填。", + "timeout-ms": "超时(ms)", + "timeout-ms-invalid-format": "超时必须是一个正整数。", + "timeout-ms-required": "超时必填。", + "user-name": "用户名", + "user-name-required": "用户名必填。" + } + }, + "dialog": { + "close": "关闭对话框" + }, + "direction": { + "column": "列", + "row": "排" + }, + "edge": { + "edge": "Edge", + "edge-instances": "Edge实例", + "edge-file": "Edge文件", + "name-max-length": "名称长度必须少于256个字符", + "label-max-length": "标签长度必须少于256个字符", + "type-max-length": "类型长度必须少于256个字符", + "management": "Edge管理", + "no-edges-matching": "没有找到匹配的Edge'{{entity}}'。", + "add": "增加Edge", + "no-edges-text": "没有找到Edge", + "edge-details": "Edge详情", + "add-edge-text": "增加新的Edge", + "delete": "删除Edge", + "delete-edge-title": "你确定删除Edge'{{edgeName}}'?", + "delete-edge-text": "请注意:确认删除后Edge以及所有关联数据将无法恢复。", + "delete-edges-title": "你确认删除Edge{ count, plural, 1 {1个Edge} other {#个Edge} }?", + "delete-edges-text": "请注意:确认删除后选定的Edge以及所有关联数据将无法恢复。", + "name": "名称", + "name-starts-with": "Edge名称前缀", + "name-required": "名称必填。", + "description": "描述", + "details": "详情", + "events": "事件", + "copy-id": "复制Edge编号", + "id-copied-message": "Edge编号已经复制到剪切板", + "sync": "同步Edge", + "edge-required": "Edge必填。", + "edge-type": "Edge类型", + "edge-type-required": "Edge类型必填。", + "event-action": "事件行动", + "entity-id": "实体编号", + "select-edge-type": "选择Edge类型", + "assign-to-customer": "分配给客户", + "assign-to-customer-text": "请选择需要分配给Edge的客户", + "assign-edge-to-customer": "分配Edge给客户", + "assign-edge-to-customer-text": "请选择需要分配给Edge的客户", + "assignedToCustomer": "分配给客户", + "edge-public": "Edge公开", + "assigned-to-customer": "分配给: {{customerTitle}}", + "unassign-from-customer": "取消分配客户", + "unassign-edge-title": "你确定取消分配Edge'{{edgeName}}'?", + "unassign-edge-text": "确定后Edge将被取消分配,并且客户将无法访问。", + "unassign-edges-title": "你确定要取消分配{count,plural,1 {1个Edge} other {#个Edge} }吗?", + "unassign-edges-text": "确定后所有选定的Edge将被取消分配,并且客户将无法访问。", + "make-public": "公开", + "make-public-edge-title": "你确定要将Edge'{{edgeName}}'设为公开吗?", + "make-public-edge-text": "确认后Edge数据将被设为公开并允许访问。", + "make-private": "私有", + "public": "公开", + "make-private-edge-title": "你确定要将Edge'{{edgeName}}'设为私有?", + "make-private-edge-text": "确认后Edge数据将被设为私有并不允许访问。", + "import": "导入Edge", + "label": "标签", + "load-entity-error": "加载数据失败,实体已经被删除。", + "assign-new-edge": "分配新Edge", + "unassign-from-edge": "取消分配Edge", + "edge-key": "Edge键", + "copy-edge-key": "复制Edge键", + "edge-key-copied-message": "Edge键已经被复制到剪切板", + "edge-secret": "Edge密钥", + "copy-edge-secret": "复制Edge密钥", + "edge-secret-copied-message": "Edge密钥已经被复制到剪切板", + "edge-assets": "Edge资产", + "edge-devices": "Edge设备", + "edge-entity-views": "Edge实体视图", + "edge-dashboards": "Edge仪表板", + "edge-rulechains": "Edge规则链", + "assets": "Edge资产", + "devices": "Edge设备", + "entity-views": "Edge实体视图", + "dashboard": "Edge仪表板", + "dashboards": "Edge仪表板", + "rulechain-templates": "规则链模版", + "rulechains": "规则链", + "search": "搜索Edge", + "selected-edges": "已选择{ count, plural, 1 {1个Edge} other {#个Edge} }", + "any-edge": "任何Edge", + "no-edge-types-matching": "没有找到匹配的Edge类型'{{entitySubtype}}'。", + "edge-type-list-empty": "没有选择Edge类型。", + "edge-types": "Edge类型", + "enter-edge-type": "输入Edge类型", + "deployed": "已部署", + "pending": "待定", + "downlinks": "下行", + "no-downlinks-prompt": "没有找到下行", + "sync-process-started-successfully": "同步处理开始成功!", + "missing-related-rule-chains-title": "Edge缺少关联规则链", + "missing-related-rule-chains-text": "分配给Edge的规则链使用规则节点将消息转发给未分配给当前Edge的规则链。

缺少的规则链列表:
{{missingRuleChains}}", + "widget-datasource-error": "组件只支持Edge实体数据源" + }, + "edge-event": { + "type-dashboard": "仪表板", + "type-asset": "资产", + "type-device": "设备", + "type-device-profile": "设备配置", + "type-asset-profile": "资产配置", + "type-entity-view": "实体视图", + "type-alarm": "告警", + "type-rule-chain": "规则链", + "type-rule-chain-metadata": "规则链元数据", + "type-edge": "Edge", + "type-user": "用户", + "type-customer": "客户", + "type-relation": "关联", + "type-widgets-bundle": "部件包", + "type-widgets-type": "部件类型", + "type-admin-settings": "管理员设置", + "action-type-added": "增加", + "action-type-deleted": "删除", + "action-type-updated": "更新", + "action-type-post-attributes": "推送属性", + "action-type-attributes-updated": "属性更新", + "action-type-attributes-deleted": "属性删除", + "action-type-timeseries-updated": "时序更新", + "action-type-credentials-updated": "认证更新", + "action-type-assigned-to-customer": "分配给客户", + "action-type-unassigned-from-customer": "取消分配客户", + "action-type-relation-add-or-update": "关联增加或更新", + "action-type-relation-deleted": "关联删除", + "action-type-rpc-call": "RPC调用", + "action-type-alarm-ack": "警报确认", + "action-type-alarm-clear": "警报清除", + "action-type-assigned-to-edge": "分配给Edge", + "action-type-unassigned-from-edge": "取消分配Edge", + "action-type-credentials-request": "认证请求", + "action-type-entity-merge-request": "实体合并请求" + }, + "error": { + "unable-to-connect": "无法连接到服务器,请检查你的互联网连接。", + "unhandled-error-code": "未处理的错误代码: {{errorCode}}", + "unknown-error": "未知错误" + }, + "entity": { + "entity": "实体", + "entities": "实体", + "entities-count": "实体数量", + "aliases": "实体别名", + "entity-alias": "实体别名", + "unable-delete-entity-alias-title": "无法删除实体别名", + "unable-delete-entity-alias-text": "实体别名'{{entityAlias}}'被以下部件使用不能删除:
{{widgetsList}}", + "duplicate-alias-error": "别名'{{alias}}'重复。
同一仪表板别名必须唯一。", + "missing-entity-filter-error": "别名'{{alias}}'缺少过滤器", + "configure-alias": "别名'{{alias}}'配置", + "alias": "别名", + "alias-required": "实体别名必填。", + "remove-alias": "移除实体别名", + "add-alias": "添加实体别名", + "entity-list": "实体列表", + "entity-type": "实体类型", + "entity-types": "实体类型", + "entity-type-list": "实体类型列表", + "any-entity": "任意实体", + "enter-entity-type": "输入实体类型", + "no-entities-matching": "没有找到符合'{{entity}}'的实体。", + "no-entity-types-matching": "没有找到符合'{{entityType}}'类型的实体。", + "name-starts-with": "名称开始于", + "help-text": "根据需要可以使用'%'进行匹配,例如:'%entity_name_contains%', '%entity_name_ends', 'entity_starts_with'。", + "use-entity-name-filter": "用户过滤器", + "entity-list-empty": "没有选择实体。", + "entity-type-list-empty": "没有选择实体类型。", + "entity-name-filter-required": "实体名过滤器必填。", + "entity-name-filter-no-entity-matched": "没有找到以'{{entity}}'开头的实体", + "all-subtypes": "全部", + "select-entities": "选择实体", + "no-aliases-found": "没有找到别名", + "no-alias-matching": "没有找到'{{alias}}'", + "create-new-alias": "创建新的别名!", + "key": "数据键", + "key-name": "键名", + "no-keys-found": "没有找到键", + "no-key-matching": "没有找到键'{{key}}'", + "create-new-key": "创建新的键!", + "type": "类型", + "type-required": "实体类型必填。", + "type-device": "设备", + "type-devices": "设备", + "list-of-devices": "{ count, plural, 1 {1个设备} other {列表#个设备} }", + "device-name-starts-with": "以'{{prefix}}'开头的设备", + "type-device-profile": "设备配置", + "type-device-profiles": "设备配置", + "list-of-device-profiles": "{ count, plural, 1 {1个设备配置} other {列表#个设备配置} }", + "device-profile-name-starts-with": "名称以'{{prefix}}'开头的设备配置", + "type-asset-profile": "资产配置", + "type-asset-profiles": "资产配置", + "list-of-asset-profiles": "{ count, plural, 1 {1个资产配置} other {列表#资产配置} }", + "asset-profile-name-starts-with": "名称以'{{prefix}}'开头的资产配置", + "type-asset": "资产", + "type-assets": "资产", + "list-of-assets": "{ count, plural, 1 {1个资产} other {列表#个资产} }", + "asset-name-starts-with": "以 '{{prefix}}' 开头的资产", + "type-entity-view": "实体视图", + "type-entity-views": "实体视图", + "list-of-entity-views": "{ count, plural, 1 {1个实体视图} other {列表#个实体视图} }", + "entity-view-name-starts-with": "以'{{prefix}}'开头的实体视图", + "type-rule": "规则", + "type-rules": "规则", + "list-of-rules": "{ count, plural, 1 {1个规则} other {列表#个规则} }", + "rule-name-starts-with": "以'{{prefix}}' 开头的规则", + "type-plugin": "插件", + "type-plugins": "插件", + "list-of-plugins": "{ count, plural, 1 {1个插件} other {列表#个插件} }", + "plugin-name-starts-with": "以'{{prefix}}'开头的插件", + "type-tenant": "租户", + "type-tenants": "租户", + "list-of-tenants": "{ count, plural, 1 {1个租户} other {列表#个租户} }", + "tenant-name-starts-with": "以'{{prefix}}'开头的租户", + "type-tenant-profile": "租户简介", + "type-tenant-profiles": "租户配置", + "list-of-tenant-profiles": "{ count, plural, 1 {1个租户配置} other {列表#个租户配置} }", + "tenant-profile-name-starts-with": "名称以'{{prefix}}'开头的租户配置", + "type-customer": "客户", + "type-customers": "客户", + "list-of-customers": "{ count, plural, 1 {1个客户} other {列表#个客户} }", + "customer-name-starts-with": "以'{{prefix}}'开头的客户", + "type-user": "用户", + "type-users": "用户", + "list-of-users": "{ count, plural, 1 {1个用户} other {列表#个用户} }", + "user-name-starts-with": "以'{{prefix}}'开头的用户", + "type-dashboard": "仪表板", + "type-dashboards": "仪表板", + "list-of-dashboards": "{ count, plural, 1 {1个仪表板} other {列表#个仪表板} }", + "dashboard-name-starts-with": "以'{{prefix}}'开头的仪表板", + "type-alarm": "警报", + "type-alarms": "警报", + "list-of-alarms": "{ count, plural, 1 {1个警报} other {列表#个警报} }", + "alarm-name-starts-with": "以'{{prefix}}'开头的警报", + "type-rulechain": "规则链", + "type-rulechains": "规则链库", + "list-of-rulechains": "{ count, plural, 1 {1个规则链} other {列表#个规则链} }", + "rulechain-name-starts-with": "规则链前缀名称'{{prefix}}'", + "type-rulenode": "规则节点", + "type-rulenodes": "规则节点", + "list-of-rulenodes": "{ count, plural, 1 {1个规则节点} other {列表#个规则节点} }", + "rulenode-name-starts-with": "名称以'{{prefix}}'开头的规则节点", + "type-current-customer": "当前客户", + "type-current-tenant": "当前租户", + "type-current-user": "当前用户", + "type-current-user-owner": "当前用户所有者", + "type-widgets-bundle": "部件包", + "type-widgets-bundles": "部件包", + "list-of-widgets-bundles": "{ count, plural, 1 {1个部件包} other {列表#部件包} }", + "search": "实体检索", + "selected-entities": "已选择 { count, plural, 1 {1个实体} other {#个实体} }", + "entity-name": "实体名", + "entity-label": "实体标签", + "details": "实体详情", + "no-entities-prompt": "没有找到实体", + "no-data": "无数据", + "columns-to-display": "要显示的列", + "type-api-usage-state": "Api状态统计", + "type-edge": "Edge", + "type-edges": "Edge", + "list-of-edges": "{ count, plural, 1 {1个Edge} other {列表#个Edge} }", + "edge-name-starts-with": "以'{{prefix}}'开头的Edge", + "type-tb-resource": "资源", + "type-ota-package": "OTA包" + }, + "entity-field": { + "created-time": "创建时间", + "name": "名称", + "type": "类型", + "first-name": "名字", + "last-name": "姓", + "email": "电子邮件", + "title": "标题", + "country": "国家", + "state": "省/州", + "city": "城市", + "address": "地址", + "address2": "地址2", + "zip": "邮政编码", + "phone": "电话", + "label": "标签" + }, + "entity-view": { + "entity-view": "实体视图", + "entity-view-required": "实体视图必填。", + "entity-views": "实体视图", + "management": "实体视图管理", + "view-entity-views": "查看实体视图", + "entity-view-alias": "实体视图别名", + "aliases": "实体视图别名", + "no-alias-matching": "'{{alias}}'没有找到。", + "no-aliases-found": "找不到别名。", + "no-key-matching": "'{{key}}'没有找到。", + "no-keys-found": "找不到密钥。", + "create-new-alias": "创建新的别名!", + "create-new-key": "创建新的键!", + "duplicate-alias-error": "找到重复别名'{{alias}}'。
实体视图别名必须是唯一的。", + "configure-alias": "配置'{{alias}}'别名", + "no-entity-views-matching": "找不到与'{{entity}}'匹配的实体视图。", + "public": "公开", + "alias": "别名", + "alias-required": "实体视图别名必填。", + "remove-alias": "移除实体视图别名", + "add-alias": "添加实体视图别名", + "name-starts-with": "名称前缀", + "help-text": "根据需要可以使用'%'进行匹配,例如:'%entity-view_name_contains%', '%entity-view_name_ends', 'entity-view_starts_with'。", + "entity-view-list": "实体视图列表", + "use-entity-view-name-filter": "使用过滤器", + "entity-view-list-empty": "没有被选中的实体视图", + "entity-view-name-filter-required": "实体视图名称过滤器必填。", + "entity-view-name-filter-no-entity-view-matched": "找不到以'{{entityView}}'开头的实体视图。", + "add": "添加实体视图", + "entity-view-public": "实体视图是公开的", + "assign-to-customer": "分配给客户", + "assign-entity-view-to-customer": "将实体视图分配给客户", + "assign-entity-view-to-customer-text": "请选择要分配给客户的实体视图", + "assign-entity-view-to-edge-title": "将实体视图分配给Edge", + "no-entity-views-text": "找不到实体视图", + "assign-to-customer-text": "请选择客户分配实体视图", + "entity-view-details": "实体视图详细信息", + "add-entity-view-text": "添加新实体视图", + "delete": "删除实体视图", + "assign-entity-views": "分配实体视图", + "assign-entity-views-text": "分配{ count, plural, 1 {1个实体视图} other {#个实体视图} }给客户", + "delete-entity-views": "删除实体视图", + "unassign-from-customer": "取消分配客户", + "unassign-entity-views": "取消分配实体视图", + "unassign-entity-views-action-title": "从客户处取消分配{count,plural,1 {#实体视图} other {#实体视图} }", + "assign-new-entity-view": "分配新实体视图", + "delete-entity-view-title": "确定要删除实体视图'{{entityViewName}}'?", + "delete-entity-view-text": "请注意:确认删除后实体视图及其所有数据将无法恢复。", + "delete-entity-views-title": "确定要删除{ count, plural, 1 {1实体视图} other {#实体视图} }?", + "delete-entity-views-action-title": "删除{ count, plural, 1 {1个实体视图} other {#个实体视图} }", + "delete-entity-views-text": "请注意:确认删除后所有选定的实体视图后,所有的数据将无法恢复。", + "unassign-entity-view-title": "你确定要取消对'{{entityViewName}}'实体视图的分配吗?", + "unassign-entity-view-text": "确认后实体视图将取消分配,客户无法访问。", + "unassign-entity-view": "取消分配实体视图", + "unassign-entity-views-title": "确定要取消分配{ count, plural, 1 {1个实体视图} other {#个实体视图} }吗?", + "unassign-entity-views-text": "确认后所选定的实体视图将被取消分配,客户无法访问。", + "entity-view-type": "实体视图类型", + "entity-view-type-required": "实体视图类型必填。", + "select-entity-view-type": "选择实体视图类型", + "enter-entity-view-type": "输入实体视图类型", + "any-entity-view": "任何实体视图", + "no-entity-view-types-matching": "没有找到匹配'{{entitySubtype}}'的实体视图类型。", + "entity-view-type-list-empty": "实体视图类型未选择。", + "entity-view-types": "实体视图类型", + "created-time": "创建时间", + "name": "名称", + "name-required": "名称必填。", + "name-max-length": "名称长度不能大于256", + "type-max-length": "实体视图类型长度不能大于256", + "description": "描述", + "events": "事件", + "details": "详情", + "copyId": "复制实体视图ID", + "idCopiedMessage": "实体视图Id已复制到剪贴板", + "assignedToCustomer": "分配给客户", + "unable-entity-view-device-alias-title": "无法删除实体视图别名", + "unable-entity-view-device-alias-text": "实体视图别名'{{entityViewAlias}}'不能够被删除,因为它被下列部件所使用:
{{widgetsList}}", + "select-entity-view": "选择实体视图", + "make-public": "实体视图设为公开", + "make-private": "实体视图设为私有", + "start-date": "开始日期", + "start-ts": "开始时间", + "end-date": "结束日期", + "end-ts": "结束时间", + "date-limits": "日期限制", + "client-attributes": "客户端属性", + "shared-attributes": "共享属性", + "server-attributes": "服务端属性", + "timeseries": "时间序列", + "client-attributes-placeholder": "客户端属性", + "shared-attributes-placeholder": "共享属性", + "server-attributes-placeholder": "服务端属性", + "timeseries-placeholder": "时间序列", + "target-entity": "目标实体", + "attributes-propagation": "属性传播", + "attributes-propagation-hint": "每次保存或更新这个实体视图时将自动从目标实体复制指定的属性。由于性能原因目标实体属性不会在每次属性更改时传递到实体视图。你可以通过配置\"copy to view\"规则链中的规则节点,并将\"Post attributes\"和\"attributes Updated\"消息链接到新规则节点,从而启用自动传递。", + "timeseries-data": "时间序列数据", + "timeseries-data-hint": "配置目标实体的时间序列数据键,以便实体视图可以访问这些键。此时间序列数据是只读的。", + "search": "查找实体视图", + "selected-entity-views": "已选择{ count, plural, 1 {1个实体视图} other {#个实体视图} }", + "make-public-entity-view-title": "你确定你想创建公开'{{entityViewName}}'实体视图?", + "make-public-entity-view-text": "确认后实体视图数据将被公开并允许访问。", + "make-private-entity-view-title": "你确定你想创建私有'{{entityViewName}}'实体视图?", + "make-private-entity-view-text": "确认后实体视图数据将被私有并不允许访问。", + "assign-entity-view-to-edge": "将实体视图分配给Edge", + "assign-entity-view-to-edge-text": "请选择要分配给Edge的实体视图", + "unassign-entity-view-from-edge-title": "你确定要取消对'{{entityViewName}}'实体视图的分配吗?", + "unassign-entity-view-from-edge-text": "确认后实体视图将取消分配,Edge无法访问。", + "unassign-entity-views-from-edge-action-title": "从Edge处取消分配{count,plural,1 {1个实体视图} other {#个实体视图} }", + "unassign-entity-view-from-edge": "未分配实体视图", + "unassign-entity-views-from-edge-title": "确定要取消分配{ count, plural, 1 {1个实体视图} other {#个实体视图} }吗?", + "unassign-entity-views-from-edge-text": "确认后实体视图将被分配,Edge无法访问。" + }, + "event": { + "event-type": "事件类型", + "events-filter": "事件过滤器", + "clean-events": "清除事件", + "type-error": "错误", + "type-lc-event": "生命周期事件", + "type-stats": "类型统计", + "type-debug-rule-node": "调试", + "type-debug-rule-chain": "调试", + "no-events-prompt": "找不到事件", + "error": "错误", + "alarm": "报警", + "event-time": "事件时间", + "server": "服务器", + "body": "整体", + "method": "方法", + "type": "类型", + "message": "Message", + "message-id": "消息ID", + "copy-message-id": "Copy message Id", + "message-type": "消息类型", + "data-type": "数据类型", + "relation-type": "关联类型", + "metadata": "元数据", + "data": "数据", + "event": "事件", + "status": "状态", + "success": "成功", + "failed": "失败", + "messages-processed": "消息处理", + "max-messages-processed": "最多处理消息", + "min-messages-processed": "最少处理消息", + "errors-occurred": "错误发生", + "max-errors-occurred": "最多错误", + "min-errors-occurred": "最少错误", + "min-value": "最小值是0。", + "all-events": "全部", + "has-error": "有错误", + "entity-id": "实体ID", + "copy-entity-id": "Copy entity Id", + "entity-type": "实体类型", + "clear-filter": "清除过滤器", + "clear-request-title": "清除所有事件", + "clear-request-text": "确除清空所有事件?" + }, + "extension": { + "extensions": "扩展", + "selected-extensions": "已选择{ count, plural, 1 {1个扩展} other {#个扩展} }", + "type": "类型", + "key": "数据键", + "value": "数据值", + "id": "ID", + "extension-id": "扩展ID", + "extension-type": "扩展类型", + "transformer-json": "JSON *", + "unique-id-required": "当前扩展ID已经存在。", + "delete": "删除扩展", + "add": "添加扩展", + "edit": "编辑扩展", + "delete-extension-title": "确实要删除扩展名'{{extensionId}}'吗?", + "delete-extension-text": "请注意:确认删除后扩展数据将无法恢复。", + "delete-extensions-title": "确定要删除{ count, plural, 1 {1个扩展} other {#个扩展} }吗?", + "delete-extensions-text": "请注意:确认删除后扩展数据将无法恢复。", + "converters": "转换器", + "converter-id": "转换器序号", + "configuration": "配置", + "converter-configurations": "转换器的配置", + "token": "安全令牌", + "add-converter": "添加转换器", + "add-config": "添加转换器配置", + "device-name-expression": "设备名称表达式", + "device-type-expression": "设备类型表达式", + "custom": "定制", + "to-double": "加倍", + "transformer": "转换器", + "json-required": "转换器JSON必填。", + "json-parse": "无法解析转换器JSON。", + "attributes": "属性", + "add-attribute": "添加属性", + "add-map": "添加映射元素", + "timeseries": "时间序列", + "add-timeseries": "添加时间序列", + "field-required": "必填字段", + "brokers": "代理服务器组", + "add-broker": "添加代理服务器", + "host": "主机", + "port": "端口", + "port-range": "端口应该在1到65535的范围内。", + "ssl": "Ssl", + "credentials": "证书", + "username": "用户名", + "password": "密码", + "retry-interval": "以毫秒为单位的重试间隔", + "anonymous": "匿名", + "basic": "基本", + "pem": "PEM", + "ca-cert": "CA certificate file *", + "private-key": "Private key file *", + "cert": "Certificate file *", + "no-file": "没有选择文件。", + "drop-file": "拖拽图像或", + "mapping": "映射", + "topic-filter": "Topic过滤器", + "converter-type": "转换类型", + "converter-json": "Json", + "json-name-expression": "设备名称JSON表达式", + "topic-name-expression": "设备名称主题表达式", + "json-type-expression": "设备类型JSON表达式", + "topic-type-expression": "设备类型主题表达式", + "attribute-key-expression": "属性键名表达式", + "attr-json-key-expression": "属性键JSON表达式", + "attr-topic-key-expression": "属性键名Topic表达式", + "request-id-expression": "请求ID表达式", + "request-id-json-expression": "请求ID JSON表达式", + "request-id-topic-expression": "请求ID主题表达式", + "response-topic-expression": "响应主题表达式", + "value-expression": "值表达式", + "topic": "主题", + "timeout": "超时ms", + "converter-json-required": "转换JSON必填。", + "converter-json-parse": "无法解析转换JSON。", + "filter-expression": "筛选条件表达式", + "connect-requests": "连接请求", + "add-connect-request": "添加连接请求", + "disconnect-requests": "断开请求", + "add-disconnect-request": "添加断开请求", + "attribute-requests": "属性请求", + "add-attribute-request": "添加属性请求", + "attribute-updates": "属性更新", + "add-attribute-update": "添加属性更新", + "server-side-rpc": "服务端RPC", + "add-server-side-rpc-request": "添加服务端RPC请求", + "device-name-filter": "设备名称过滤器", + "attribute-filter": "属性过滤器", + "method-filter": "方法过滤器", + "request-topic-expression": "请求主题表达式", + "response-timeout": "毫秒内响应超时", + "topic-expression": "主题表达", + "client-scope": "客户范围", + "add-device": "添加服务器", + "opc-server": "服务器组", + "opc-add-server": "添加服务器", + "opc-add-server-prompt": "请添加服务器", + "opc-application-name": "应用名称", + "opc-application-uri": "应用URI", + "opc-scan-period-in-seconds": "秒级扫描周期", + "opc-security": "安全性", + "opc-identity": "身份", + "opc-keystore": "密钥库", + "opc-type": "类型", + "opc-keystore-type": "类型", + "opc-keystore-location": "Location *", + "opc-keystore-password": "密码", + "opc-keystore-alias": "别名", + "opc-keystore-key-password": "密钥密码", + "opc-device-node-pattern": "设备节点模式", + "opc-device-name-pattern": "设备名称模式", + "modbus-server": "Servers/slaves", + "modbus-add-server": "添加server/slave", + "modbus-add-server-prompt": "请添加server/slave", + "modbus-transport": "传输", + "modbus-tcp-reconnect": "自动重新连接", + "modbus-rtu-over-tcp": "RTU over TCP", + "modbus-port-name": "串口名称", + "modbus-encoding": "编码", + "modbus-parity": "奇偶校验", + "modbus-baudrate": "波特率", + "modbus-databits": "数据位", + "modbus-stopbits": "停止位", + "modbus-databits-range": "数据位应该在7到8的范围内。", + "modbus-stopbits-range": "停止位应该在1到2的范围内。", + "modbus-unit-id": "单位编号", + "modbus-unit-id-range": "单位ID应该在1到247的范围内", + "modbus-device-name": "设备名称", + "modbus-poll-period": "轮询周期(ms)", + "modbus-attributes-poll-period": "轮询属性周期(ms)", + "modbus-timeseries-poll-period": "时间序列轮询周期(ms)", + "modbus-poll-period-range": "轮询周期应为正值。", + "modbus-tag": "标签", + "modbus-function": "函数", + "modbus-register-address": "寄存器地址", + "modbus-register-address-range": "寄存器地址应该在0到65535的范围内。", + "modbus-register-bit-index": "位索引", + "modbus-register-bit-index-range": "位索引应该在0到15的范围内。", + "modbus-register-count": "寄存器计数", + "modbus-register-count-range": "寄存器计数应该是一个正值。", + "modbus-byte-order": "字节顺序", + "sync": { + "status": "状态", + "sync": "同步", + "not-sync": "不同步", + "last-sync-time": "最后同步时间", + "not-available": "无法使用" + }, + "export-extensions-configuration": "导出扩展配置", + "import-extensions-configuration": "导入扩展配置", + "import-extensions": "导入扩展", + "import-extension": "导入扩展", + "export-extension": "导出扩展", + "file": "扩展文件", + "invalid-file-error": "无效的扩展文件" + }, + "filter": { + "add": "添加过滤器", + "edit": "编辑过滤器", + "name": "过滤器名称", + "name-required": "过滤器名称必填。", + "duplicate-filter": "同名过滤器已存在。", + "filters": "过滤器", + "unable-delete-filter-title": "无法删除过滤器", + "unable-delete-filter-text": "无法删除过滤器'{{filter}}'因为它由以下小部件使用:
{{widgetsList}}", + "duplicate-filter-error": "找到重复的过滤器'{{filter}}'。
过滤器在仪表板中必须是唯一的。", + "missing-key-filters-error": "过滤器'{{filter}}'的键名筛选条件缺失。", + "filter": "过滤器", + "editable": "可编辑", + "no-filters-found": "找不到过滤器。", + "no-filter-text": "未指定过滤器", + "add-filter-prompt": "请添加过滤器", + "no-filter-matching": "找不到'{{filter}}'。", + "create-new-filter": "创建新过滤器!", + "filter-required": "过滤器必填。", + "operation": { + "operation": "操作", + "equal": "等于", + "not-equal": "不等于", + "starts-with": "开始于", + "ends-with": "结束于", + "contains": "包含", + "not-contains": "不包含", + "greater": "大于", + "less": "小于", + "greater-or-equal": "大于或等于", + "less-or-equal": "小于或等于", + "and": "和", + "or": "或", + "in": "匹配", + "not-in": "不匹配" + }, + "ignore-case": "忽略大小写", + "value": "数据值", + "remove-filter": "移除过滤器", + "preview": "过滤器预览", + "no-filters": "未配置过滤器", + "add-filter": "添加过滤器", + "add-complex-filter": "添加复合过滤器", + "add-complex": "添加复合", + "complex-filter": "复合过滤器", + "edit-complex-filter": "编辑复合过滤器", + "edit-filter-user-params": "编辑过滤器谓词用户参数", + "filter-user-params": "过滤谓词用户参数", + "user-parameters": "用户参数", + "display-label": "要显示的标签", + "autogenerated-label": "自动生成标签", + "order-priority": "字段顺序优先级", + "key-filter": "键名过滤器", + "key-filters": "键名过滤器", + "key-name": "键名", + "key-name-required": "键名必填。", + "key-type": { + "key-type": "键类型", + "attribute": "属性", + "timeseries": "时间序列", + "entity-field": "实体", + "constant": "常量" + }, + "value-type": { + "value-type": "值类型", + "string": "字符串", + "numeric": "数字", + "boolean": "布尔值", + "date-time": "日期时间" + }, + "value-type-required": "键值类型是必需的。", + "key-value-type-change-title": "是否确实要更改键值类型?", + "key-value-type-change-message": "如果你确认新的值类型,所有输入的键过滤器将被删除。", + "no-key-filters": "未配置键名过滤器", + "add-key-filter": "添加键名过滤器", + "remove-key-filter": "移除键名过滤器", + "edit-key-filter": "编辑键名过滤器", + "date": "日期", + "time": "时间", + "current-tenant": "当前租户", + "current-customer": "当前客户", + "current-user": "当前用户", + "current-device": "当前设备", + "default-value": "默认值", + "dynamic-source-type": "动态源类型", + "dynamic-value": "Dynamic value", + "no-dynamic-value": "无动态值", + "source-attribute": "源属性", + "switch-to-dynamic-value": "切换到动态值", + "switch-to-default-value": "切换到默认值", + "inherit-owner": "从所有者继承", + "source-attribute-not-set": "如果未设置源属性" + }, + "fullscreen": { + "expand": "展开到全屏", + "exit": "退出全屏", + "toggle": "切换全屏模式", + "fullscreen": "全屏" + }, + "function": { + "function": "函数" + }, + "gateway": { + "add-entry": "添加配置", + "connector-add": "添加新连接器", + "connector-enabled": "启用连接器", + "connector-name": "连接器名称", + "connector-name-required": "连接器名称名称必填。", + "connector-type": "连接器类型", + "connector-type-required": "连接器名称类型必填。", + "connectors": "连接器配置", + "create-new-gateway": "创建新网关", + "create-new-gateway-text": "是否确实要创建名为'{{gatewayName}}'的新网关?", + "delete": "删除配置", + "download-tip": "下载配置", + "gateway": "网关", + "gateway-exists": "同名设备已存在。", + "gateway-name": "网关名称", + "gateway-name-required": "网关名称必填。", + "gateway-saved": "已成功保存网关配置。", + "json-parse": "无效的JSON。", + "json-required": "字段不能为空。", + "no-connectors": "无连接器", + "no-data": "没有配置", + "no-gateway-found": "找不到网关。", + "no-gateway-matching": "找不到'{{item}}' 。", + "path-logs": "日志文件的路径", + "path-logs-required": "路径是必需的。", + "remote": "远程配置", + "remote-logging-level": "日志记录级别", + "remove-entry": "移除配置", + "save-tip": "保存配置", + "security-type": "安全类型", + "security-types": { + "access-token": "访问令牌", + "tls": "TLS" + }, + "storage": "存储", + "storage-max-file-records": "文件中的最大记录数", + "storage-max-files": "最大文件数", + "storage-max-files-min": "最小值为1。", + "storage-max-files-pattern": "数字无效。", + "storage-max-files-required": "数字是必需的。", + "storage-max-records": "存储中的最大记录数", + "storage-max-records-min": "最小记录数为1。", + "storage-max-records-pattern": "数字无效。", + "storage-max-records-required": "最大记录项必填。", + "storage-pack-size": "最大事件包大小", + "storage-pack-size-min": "最小值为1。", + "storage-pack-size-pattern": "数字无效。", + "storage-pack-size-required": "最大事件包大小必填。", + "storage-path": "存储路径", + "storage-path-required": "存储路径必填。", + "storage-type": "存储类型", + "storage-types": { + "file-storage": "文件存储", + "memory-storage": "内存存储" + }, + "thingsboard": "ThingsBoard", + "thingsboard-host": "ThingsBoard主机", + "thingsboard-host-required": "主机必填。", + "thingsboard-port": "ThingsBoard端口", + "thingsboard-port-max": "最大端口号为65535。", + "thingsboard-port-min": "最小端口号为1。", + "thingsboard-port-pattern": "端口无效。", + "thingsboard-port-required": "端口必填。", + "tidy": "整洁", + "tidy-tip": "整理配置JSON", + "title-connectors-json": "连接器{{typeName}}配置", + "tls-path-ca-certificate": "网关上CA证书的路径", + "tls-path-client-certificate": "网关上客户端证书的路径", + "tls-path-private-key": "网关上私钥的路径", + "toggle-fullscreen": "切换全屏", + "transformer-json-config": "配置JSON*", + "update-config": "添加/更新配置JSON" + }, + "grid": { + "delete-item-title": "你确定要删除此项吗?", + "delete-item-text": "注意,确认删除后此项数据将无法恢复。", + "delete-items-title": "你确定要删除{ count, plural, 1 {1条目} other {#条目} }吗?", + "delete-items-action-title": "删除{ count, plural, 1 {1个元素} other {#个元素} }", + "delete-items-text": "注意,确认删除后所有数据将无法恢复。", + "add-item-text": "添加新条目", + "no-items-text": "没有找到条目", + "item-details": "详细信息", + "delete-item": "删除条目", + "delete-items": "删除条目", + "scroll-to-top": "滚动到顶部" + }, + "help": { + "goto-help-page": "查看帮助", + "show-help": "显示帮助" + }, + "home": { + "home": "首页", + "profile": "属性", + "logout": "注销", + "menu": "菜单", + "avatar": "头像", + "open-user-menu": "打开用户菜单" + }, + "file-input": { + "browse-file": "浏览文件", + "browse-files": "浏览文件" + }, + "image-input": { + "drop-image-or": "拖拽图片或", + "drop-images-or": "拖拽图片或", + "no-images": "没有图片", + "images": "图片" + }, + "import": { + "no-file": "没有选择文件", + "drop-file": "拖拽文件。", + "drop-json-file-or": "拖拽JSON文件或", + "drop-file-csv": "拖拽CSV文件或", + "drop-file-csv-or": "拖拽CSV文件或", + "column-value": "数值", + "column-title": "标题", + "column-example": "示例值数据", + "column-key": "属性/遥测键", + "credentials": "证书", + "csv-delimiter": "CSV分隔符", + "csv-first-line-header": "第一行包含列名", + "csv-update-data": "更新属性/遥测", + "details": "详情", + "import-csv-number-columns-error": "一个文件至少应该包含两列", + "import-csv-invalid-format-error": "文件格式无效检查第'{{line}}'行数据。", + "column-type": { + "name": "名称", + "type": "类型", + "label": "标签", + "column-type": "列类型", + "client-attribute": "客户端属性", + "shared-attribute": "共享属性", + "server-attribute": "服务器属性", + "timeseries": "时间序列", + "entity-field": "实体字段", + "access-token": "访问令牌", + "x509": "X.509", + "mqtt": { + "client-id": "MQTT客户端ID", + "user-name": "MQTT用户名", + "password": "MQTT密码" + }, + "lwm2m": { + "client-endpoint": "客户端终节点名称", + "security-config-mode": "安全配置模式", + "client-identity": "客户标识", + "client-key": "客户端公钥", + "client-cert": "客户端证书", + "bootstrap-server-security-mode": "LwM2M bootstrap server安全模式", + "bootstrap-server-secret-key": "LwM2M bootstrap server密钥", + "bootstrap-server-public-key-id": "LwM2M bootstrap server公钥", + "lwm2m-server-security-mode": "LwM2M server安全模式", + "lwm2m-server-secret-key": "LwM2M server证书密钥", + "lwm2m-server-public-key-id": "LwM2M server公钥" + }, + "isgateway": "是否网关", + "activity-time-from-gateway-device": "网关设备活动时间", + "description": "描述", + "routing-key": "Edge键", + "secret": "Edge密钥" + }, + "stepper-text": { + "select-file": "选择一个文件", + "configuration": "导入配置", + "column-type": "选择列类型", + "creat-entities": "创建新实体" + }, + "message": { + "create-entities": "{{count}}个新实体已成功创建。", + "update-entities": "{{count}}个实体已成功更新。", + "error-entities": "创建{{count}}个实体时出错。" + } + }, + "item": { + "selected": "选定" + }, + "js-func": { + "no-return-error": "函数必须返回值!", + "return-type-mismatch": "函数必须返回'{{type}}'类型的值!", + "tidy": "整洁", + "mini": "迷你" + }, + "key-val": { + "key": "数据键", + "value": "数据值", + "remove-entry": "移除", + "add-entry": "添加", + "no-data": "无数据" + }, + "layout": { + "layout": "布局", + "manage": "布局管理", + "settings": "布局设置", + "color": "颜色", + "main": "主体", + "right": "右侧", + "left": "Left", + "select": "选择布局", + "percentage-width": "Percentage width (%)", + "fixed-width": "Fixed width (px)", + "left-width": "Left column (%)", + "right-width": "Right column (%)", + "pick-fixed-side": "Fixed side: ", + "layout-fixed-width": "Fixed width (px)", + "value-min-error": "Value must be more then {{min}}{{unit}}", + "value-max-error": "Value must be less then {{max}}{{unit}}", + "layout-fixed-width-required": "Fixed width is required", + "right-width-percentage-required": "Right percentage is required", + "left-width-percentage-required": "Left percentage is required", + "divider": "Divider", + "right-side": "Right side layout", + "left-side": "Left side layout" + }, + "legend": { + "direction": "图例方向", + "position": "图例位置", + "sort-legend": "在图例中排序数据键", + "show-max": "显示最大值", + "show-min": "显示最小值", + "show-avg": "显示平均值", + "show-total": "显示总数", + "show-latest": "显示最新数据值", + "settings": "图例设置", + "min": "最小值", + "max": "最大值", + "avg": "平均值", + "total": "总数", + "latest": "最新值", + "comparison-time-ago": { + "previousInterval": "(历史间隔)", + "customInterval": "(自定义间隔)", + "days": "(一天前)", + "weeks": "(一周前)", + "months": "(一月前)", + "years": "(一年前)" + } + }, + "login": { + "login": "登录", + "request-password-reset": "请求密码恢复", + "reset-password": "恢复密码", + "create-password": "创建密码", + "two-factor-authentication": "双重因素身份验证", + "passwords-mismatch-error": "输入的密码必须相同!", + "password-again": "再次输入密码", + "sign-in": "登录 ", + "username": "用户名(电子邮件)", + "remember-me": "记住我", + "forgot-password": "忘记密码?", + "password-reset": "密码恢复", + "expired-password-reset-message": "请创建新密码凭证已失效。", + "new-password": "新密码", + "new-password-again": "再次输入新密码", + "password-link-sent-message": "密码恢复链接已成功发送!", + "email": "电子邮件", + "login-with": "使用{{name}}登录", + "or": "或", + "error": "登录错误", + "verify-your-identity": "身份你的验证", + "select-way-to-verify": "选择一个验证方法", + "resend-code": "重新发送验证码", + "resend-code-wait": "重新发送代码{ time, plural, 1 {1秒} other {#秒} }", + "try-another-way": "尝试另一种方式", + "totp-auth-description": "请输入你验证应用程序中的验证代码。", + "totp-auth-placeholder": "代码", + "sms-auth-description": "通过{{contact}}发送了验证码,已将其发送到你的手机。", + "sms-auth-placeholder": "SMS代码", + "email-auth-description": "{{Contact}}的验代码已发送到你的电子邮箱。", + "email-auth-placeholder": "Email代码", + "backup-code-auth-description": "请输入你的临时密码", + "backup-code-auth-placeholder": "临时密码" + }, + "markdown": { + "edit": "编辑", + "preview": "预览", + "copy-code": "单击拷贝", + "copied": "已拷贝!" + }, + "ota-update": { + "add": "添加包", + "assign-firmware": "分配的固件", + "assign-firmware-required": "分配的固件必填", + "assign-software": "分配的软件", + "assign-software-required": "分配的软件必填", + "auto-generate-checksum": "生成校验码", + "checksum": "校验码", + "checksum-hint": "如果校验和为空,会自动生成", + "checksum-algorithm": "校验和算法", + "checksum-copied-message": "包校验和已复制到剪贴板", + "change-firmware": "固件的更改可能会导致{ count, plural, 1 {1个设备} other {#个设备} }的更新。", + "change-software": "软件的更改可能会导致{ count, plural, 1 {1个设备} other {#个设备} }的更新。", + "chose-compatible-device-profile": "上传的包仅适用于具有所选配置文件的设备。", + "chose-firmware-distributed-device": "选择将分发到设备的固件", + "chose-software-distributed-device": "选择将分发到设备的软件", + "content-type": "内容类型", + "copy-checksum": "复制校验和", + "copy-direct-url": "复制直接URL", + "copyId": "复制包ID", + "copied": "已复制!", + "delete": "删除包", + "delete-ota-update-text": "请注意:确认删除后OTA升级数据将无法恢复。", + "delete-ota-update-title": "你确定要删除OTA升级'{{title}}'吗?", + "delete-ota-updates-text": "请注意:确认删除后所有选中的OTA升级数据将无法恢复", + "delete-ota-updates-title": "你确定要删除{ count, plural, 1 {1个OTA升级} other {#个OTA升级} }吗?", + "description": "描述", + "direct-url": "直接URL", + "direct-url-copied-message": "包直接URL已复制到剪贴板", + "direct-url-required": "直接URL必填", + "download": "下载包", + "drop-file": "拖拽文件或", + "drop-package-file-or": "拖拽文件或", + "file-name": "文件名", + "file-size": "文件大小", + "file-size-bytes": "文件大小(以字节为单位)", + "idCopiedMessage": "包ID已被复制到剪贴板", + "no-firmware-matching": "没有找到与'{{entity}}'匹配的兼容固件OTA升级包。", + "no-firmware-text": "没有提供兼容的固件OTA升级包。", + "no-packages-text": "没有找到包", + "no-software-matching": "没有找到匹配'{{entity}}'的兼容软件OTA升级包。", + "no-software-text": "没有提供兼容的软件OTA升级包。", + "ota-update": "OTA升级", + "ota-update-details": "OTA升级详情", + "ota-updates": "OTA 升级", + "package-type": "包类型", + "packages-repository": "包仓库", + "search": "搜索包", + "selected-package": "选中{ count, plural, 1 {1个包} other {#个包} }", + "title": "标题", + "title-required": "标题必填。", + "title-max-length": "标题长度应小于256", + "types": { + "firmware": "固件", + "software": "软件" + }, + "upload-binary-file": "上传二进制文件", + "use-external-url": "使用外部URL", + "version": "版本", + "version-required": "版本必填。", + "version-tag": "版本标签", + "version-tag-hint": "自定义标签应与你设备报告的软件包版本相匹配。", + "version-max-length": "版本应小于256", + "warning-after-save-no-edit": "包传后你将无法修改标题、版本、设备配置文件和包类型。" + }, + "position": { + "top": "顶部", + "bottom": "底部", + "left": "左侧", + "right": "右侧" + }, + "profile": { + "profile": "属性", + "last-login-time": "最后登录", + "change-password": "更改密码", + "current-password": "当前密码", + "copy-jwt-token": "复制JWT令牌", + "jwt-token": "JWT令牌", + "token-valid-till": "令牌有效期至", + "tokenCopiedSuccessMessage": "JWT令牌已复制到剪贴板", + "tokenCopiedWarnMessage": "JWT令牌已过期,请刷新页面。" + }, + "profiles": { + "profiles": "Profiles" + }, + "security": { + "security": "安全", + "2fa": { + "2fa": "双因素认证", + "2fa-description": "双因素身份认证可保护您的帐户免于未经授权的访问。", + "authenticate-with": "你可以使用:", + "disable-2fa-provider-text": "禁用{{name}}你的帐户将存在安全风险", + "disable-2fa-provider-title": "你确定要禁用{{name}}吗?", + "get-new-code": "获取新代码", + "main-2fa-method": "主要用于两因素身份认证方法", + "dialog": { + "activation-step-description-email": "下次登录时将提醒你输入邮箱中的验证码。", + "activation-step-description-sms": "下次登录时将提醒你输入短信中的验证码。", + "activation-step-description-totp": "下次登录时将提醒你输入双因素认证中的验证码。", + "activation-step-label": "激活", + "backup-code-description": "通守临时密码在当需要时可以很方便的将其登录到你的帐户", + "backup-code-warn": "离开此页面后临时密码将无法再次显示。", + "download-txt": "下载(txt)", + "email-step-description": "输入身份验证的电子邮件", + "email-step-label": "电子邮件", + "enable-email-title": "启用电子邮件身份验证", + "enable-sms-title": "启用短信身份验证", + "enable-totp-title": "启用应用程序身份验证", + "enter-verification-code": "输入6位验证码", + "get-backup-code-title": "获取验证码", + "next": "下一步", + "scan-qr-code": "通过APP扫描二维码", + "send-code": "发送代码", + "sms-step-description": "输入验证你身份器的电话号码。", + "sms-step-label": "电话号码", + "success": "成功!", + "totp-step-description-install": "你安装的Google身份验证,Authy或Duo等应用程序。", + "totp-step-description-open": "打开你手机上的身份验证应用程序。", + "totp-step-label": "获取应用程序", + "verification-code": "6位验证码", + "verification-code-invalid": "验证码格式错误", + "verification-code-incorrect": "验证不错误", + "verification-code-many-request": "验证码请求次数太多。", + "verification-step-description": "输入刚刚发送到{{address}}的6位验证码。", + "verification-step-label": "确认" + }, + "provider": { + "email": "邮件", + "email-description": "通过电子邮件进行身份验证。", + "email-hint": "验证码通过电子邮件发送到{{ info }}", + "sms": "短信", + "sms-description": "通过短信发送验证代码。", + "sms-hint": "验证码通过短信发送到{{ info }}", + "totp": "认证程序", + "totp-description": "通过Google认证进行身份验证。", + "totp-hint": "为你的帐户设置身份验证程序", + "backup_code": "备用验证码", + "backup-code-description": "临时密码身份验证。", + "backup-code-hint": "{{ info }}临时密码处于可用状态" + } + }, + "password-requirement": { + "at-least": "至少", + "character": "{ count, plural, 1 {1字符} other {#字符} }", + "digit": "{ count, plural, 1 {1数字} other {#数字} }", + "incorrect-password-try-again": "密码不正确请重试。", + "lowercase-letter": "{ count, plural, 1 {1小写字母} other {#小写字母} }", + "new-passwords-not-match": "新密码不匹配", + "password-should-not-contain-spaces": "你的密码不应包含空格", + "password-not-meet-requirements": "密码不符合要求", + "password-requirements": "密码要求", + "password-should-difference": "新密码应与当前密码不同", + "special-character": "{ count, plural, 1 {1特殊字符} other {#特殊字符} }", + "uppercase-letter": "{ count, plural, 1 {1大写字母} other {#大写字母} }" + } + }, + "relation": { + "relations": "关联", + "direction": "方向", + "search-direction": { + "FROM": "从", + "TO": "到" + }, + "direction-type": { + "FROM": "从", + "TO": "到" + }, + "from-relations": "向外的关联", + "to-relations": "向内的关联", + "selected-relations": "已选择{ count, plural, 1 {1个关联} other {#个关联} }", + "type": "类型", + "to-entity-type": "到实体类型", + "to-entity-name": "到实体名称", + "from-entity-type": "从实体类型", + "from-entity-name": "从实体类型", + "to-entity": "到实体", + "from-entity": "从实体", + "delete": "删除关联", + "relation-type": "关联类型", + "relation-type-required": "关联类型必填", + "relation-type-max-length": "关系类型长度小于256", + "any-relation-type": "任何类型", + "add": "添加关联", + "edit": "编辑关联", + "delete-to-relation-title": "确定要删除实体'{{entityName}}'的关联吗?", + "delete-to-relation-text": "确定删除后实体'{{entityName}}'将取消与当前实体的关联关系。", + "delete-to-relations-title": "确定要删除{ count, plural, 1 {1个关联} other {#个关联} }?", + "delete-to-relations-text": "确定删除所有选择的关联关系后,与当前实体对应的所有关联关系将被移除。", + "delete-from-relation-title": "确定要从实体'{{entityName}}'删除关联吗?", + "delete-from-relation-text": "确定删除后,当前实体将与实体 '{{entityName}}'取消关联", + "delete-from-relations-title": "确定删除{ count, plural, 1 {1个关联} other {#个关联} }吗?", + "delete-from-relations-text": "确定删除所有选择的关联关系后,当前实体将与对应的实体取消关联", + "remove-relation-filter": "移除关联过滤器", + "add-relation-filter": "添加关联过滤器", + "any-relation": "任意关联", + "relation-filters": "关联过滤器", + "additional-info": "附加信息 (JSON)", + "invalid-additional-info": "无法解析附加信息JSON。", + "no-relations-text": "未找到关联" + }, + "resource": { + "add": "添加资源", + "copyId": "复制资源ID", + "delete": "删除资源", + "delete-resource-text": "请注意:确认删除后资源数据将无法恢复。", + "delete-resource-title": "确定要删除资源'{{resourceTitle}}'吗?", + "delete-resources-action-title": "删除{ count, plural, 1 {1个资源} other {#个资源} }", + "delete-resources-text": "请注意:确认删除后资源数据将无法恢复。", + "delete-resources-title": "确定要删除{ count, plural, 1 {1个资源} other {#个资源} }?", + "download": "下载资源", + "drop-file": "拖拽资源文件或", + "drop-resource-file-or": "拖拽资源文件或", + "empty": "资源为空", + "file-name": "文件名", + "idCopiedMessage": "资源ID已经复制到粘贴板", + "no-resource-matching": "找不到与'{{widgetsBundle}}'匹配的资源。", + "no-resource-text": "找不到资源", + "open-widgets-bundle": "打开部件库", + "resource": "资源", + "resource-library-details": "资源库详情", + "resource-type": "资源类型", + "resources-library": "资源库", + "search": "查找资源", + "selected-resources": "已选择{ count, plural, 1 {1个资源} other {#个资源} }", + "system": "系统", + "title": "标题", + "title-required": "标题是必填项。", + "title-max-length": "标题长度小于256" + }, + "rulechain": { + "rulechain": "规则链", + "rulechains": "规则链", + "root": "是否根链", + "delete": "删除规则链", + "name": "名称", + "name-required": "名称必填。", + "name-max-length": "名称长度不能大于256", + "description": "描述", + "add": "添加规则链", + "set-root": "设置为根规则链", + "set-root-rulechain-title": "你确定要生成规则链'{{ruleChainName}}'根吗?", + "set-root-rulechain-text": "确认之后将变为根规格链并将处理所有接入的传输消息。", + "delete-rulechain-title": " 确实要删除规则链'{{ruleChainName}}'吗?", + "delete-rulechain-text": "请注意:在确认删除后规则链和所有数据将无法恢复。", + "delete-rulechains-title": "确实要删除{count, plural, 1 { 1个规则链} other {#个规则链} }吗?", + "delete-rulechains-action-title": "删除{ count, plural, 1 {1个规则链} other {#个规则链} }", + "delete-rulechains-text": "请注意:确认删除后所有选定的规则链数据将无法恢复。", + "add-rulechain-text": "添加新的规则链", + "no-rulechains-text": "未找到规则链", + "rulechain-details": "规则链详情", + "details": "详情", + "events": "事件", + "system": "系统", + "import": "导入规则链", + "export": "导出规则链", + "export-failed-error": "无法导出规则链:{{error}}", + "create-new-rulechain": "创建新的规则链", + "rulechain-file": "规则链文件", + "invalid-rulechain-file-error": "规则链数据结构无效导入失败。", + "copyId": "复制规则链ID", + "idCopiedMessage": "规则ID已经复制到粘贴板", + "select-rulechain": "选择规则链", + "no-rulechains-matching": "没有发现匹配'{{entity}}'的规则链。", + "rulechain-required": "规则链必填", + "management": "规则集管理", + "debug-mode": "调试模式", + "search": "查找规则链", + "selected-rulechains": "已选择{ count, plural, 1 {1个规则链} other {#个规则链} }", + "open-rulechain": "打开规则链", + "assign-new-rulechain": "分配新规则琏", + "edge-template-root": "根模版", + "assign-to-edge": "分配规则链给Edge", + "edge-rulechain": "Edge规则链", + "unassign-rulechain-from-edge-text": "确认后规则链将取消分配,Edge无法访问。", + "unassign-rulechains-from-edge-title": "你确定要取消分配规则链{ count, plural, 1 {1个规则链} other {#个规则链} }?", + "unassign-rulechains-from-edge-text": "确认后规则链将取消分配,Edge无法访问。", + "assign-rulechain-to-edge-title": "分配规则链给Edge", + "assign-rulechain-to-edge-text": "请选择要分配给Edge的规则链", + "set-edge-template-root-rulechain": "设置规则链为Edge模版根", + "set-edge-template-root-rulechain-title": "你确定将规则链'{{ruleChainName}}'设置为Edge模版根吗?", + "set-edge-template-root-rulechain-text": "确认后规则链将将会成为Edge模版根,且它会成为新创建Edge的根规则链。", + "invalid-rulechain-type-error": "不能导入规则链:无效的规则链类型。期望类型为{{expectedRuleChainType}}。", + "set-auto-assign-to-edge": "创建时分配规则链给Edge", + "set-auto-assign-to-edge-title": "你确定创建时将规则链'{{ruleChainName}}'自动分配给Edge吗", + "set-auto-assign-to-edge-text": "确认后创建时规则链将自动分配给Edge。", + "unset-auto-assign-to-edge": "创建时分配规则链给Edge", + "unset-auto-assign-to-edge-title": "你确定取消创建时将规则链'{{ruleChainName}}'自动分配给Edge吗?", + "unset-auto-assign-to-edge-text": "确认后创建时规则链将不再自动分配给Edge。", + "unassign-rulechain-title": "你确定要取消分配规则链'{{ruleChainName}}'?", + "unassign-rulechains": "取消分配规则链" + }, + "rulenode": { + "details": "详情", + "events": "事件", + "search": "查找节点", + "open-node-library": "打开节点库", + "add": "添加规则节点", + "name": "名称", + "name-required": "名称必填。", + "name-max-length": "名称长度不能大于256", + "type": "类型", + "description": "描述", + "delete": "删除规则节点", + "select-all-objects": "选择所有节点和连接", + "deselect-all-objects": "取消选择所有节点和连接", + "delete-selected-objects": "删除选定的节点和连接", + "delete-selected": "删除选定", + "create-nested-rulechain": "创建嵌套规则链", + "select-all": "选择全部", + "copy-selected": "选择副本", + "deselect-all": "取消选择", + "rulenode-details": "规则节点详情", + "debug-mode": "调试模式", + "configuration": "配置", + "link": "链接", + "link-details": "规则节点链接详情", + "add-link": "添加链接", + "link-label": "链接标签", + "link-label-required": "链接标签必填", + "custom-link-label": "自定义链接标签", + "custom-link-label-required": "自定义链接标签必填", + "link-labels": "链接标签", + "link-labels-required": "链接标签必填。", + "no-link-labels-found": "找不到链接标签", + "no-link-label-matching": "找不到'{{label}}'。", + "create-new-link-label": "创建一个新的!", + "type-filter": "过滤器", + "type-filter-details": "使用配置条件筛选传入消息", + "type-enrichment": "属性集", + "type-enrichment-details": "向消息元数据中添加附加信息", + "type-transformation": "变换", + "type-transformation-details": "更改消息 ayload和元数据", + "type-action": "动作", + "type-action-details": "执行特别动作", + "type-external": "外部的", + "type-external-details": "与外部系统交互", + "type-rule-chain": "规则链", + "type-rule-chain-details": "将传入消息转发到指定的规则链", + "type-flow": "流", + "type-flow-details": "组织消息流", + "type-input": "输入", + "type-input-details": "规则链的逻辑输入,将传入消息转发到下一个相关规则节点", + "type-unknown": "未知", + "type-unknown-details": "未解析的规则节点", + "directive-is-not-loaded": "定义的配置指令'{{directiveName}}'不可用。", + "ui-resources-load-error": "加载配置UI资源失败。", + "invalid-target-rulechain": "无法解析目标规则链!", + "test-script-function": "测试脚本功能", + "script-lang-java-script": "Java Script", + "script-lang-mvel": "MVEL", + "message": "消息", + "message-type": "消息类型", + "select-message-type": "选择消息类型", + "message-type-required": "消息类型必填", + "metadata": "元数据", + "metadata-required": "元数据项不能为空。", + "output": "输出", + "test": "测试", + "help": "帮助", + "reset-debug-mode": "恢复所有节点中的调试模式" + }, + "timezone": { + "timezone": "时区", + "select-timezone": "选择时区", + "no-timezones-matching": "找不到匹配的'{{timezone}}'时区。", + "timezone-required": "时区必填。", + "browser-time": "浏览器时间" + }, + "queue": { + "queue-name": "队列名称", + "no-queues-found": "未找到队列。", + "no-queues-matching": "找不到匹配'{{queue}}'的队列。", + "select-name": "选择队列名称", + "name": "名称", + "name-required": "队列名称必填!", + "name-unique": "队列名称必须唯一!", + "name-pattern": "队列名称包含ASCII编码的, '.', '_' 和 '-'!", + "queue-required": "队列必填!", + "topic-required": "队列主题必须!", + "poll-interval-required": "轮询间隔必填!", + "poll-interval-min-value": "轮询间隔参数不能小于1", + "partitions-required": "分区必填!", + "partitions-min-value": "分区参数不能小于1", + "pack-processing-timeout-required": "处理超时必填", + "pack-processing-timeout-min-value": "处理超时参数不能小于1", + "batch-size-required": "批量大小必填!", + "batch-size-min-value": "批量大小参数不能小于1", + "retries-required": "重试参数必填!", + "retries-min-value": "重试参数不能为负数", + "failure-percentage-required": "percentage必填!", + "failure-percentage-min-value": "percentage参数不能小于0", + "failure-percentage-max-value": "percentage参数不能大于100", + "pause-between-retries-required": "Pause和retries必填!", + "pause-between-retries-min-value": "Pause和retries参数不能小于1", + "max-pause-between-retries-required": "最大Pause和retries必填!", + "max-pause-between-retries-min-value": "最大Pause和retries参数不能小于1", + "submit-strategy-type-required": "提交策略必填!", + "processing-strategy-type-required": "处理策略必填!", + "queues": "队列", + "selected-queues": "{ count, plural, 1 {1个队列} other {#个队列} } 选中", + "delete-queue-title": "你确定要删除队列吗'{{queueName}}'?", + "delete-queues-title": "你确定你要删除 { count, plural, 1 {1个队列} other {#个队列} }?", + "delete-queue-text": "确认删除后队列和所有数据将无法恢复。", + "delete-queues-text": "确认删除后所有选定的队列将被将无法访问。", + "search": "查找对列", + "add": "添加队列", + "details": "详情", + "topic": "主题", + "submit-settings": "提交设置", + "submit-strategy": "Strategy type *", + "grouping-parameter": "分组参数", + "processing-settings": "重试处理设置", + "processing-strategy": "Processing type *", + "retries-settings": "重试设置", + "polling-settings": "轮询设置", + "batch-processing": "分区处理", + "poll-interval": "轮询间隔", + "partitions": "分区", + "immediate-processing": "超时处理", + "consumer-per-partition": "消费者分区遍历", + "consumer-per-partition-hint": "消费者分区单独启用", + "processing-timeout": "处理内部超时", + "batch-size": "批量大小", + "retries": "重试数字(0-无限)", + "failure-percentage": "跳过重试故障消息", + "pause-between-retries": "重试多少秒", + "max-pause-between-retries": "其它重试多少秒", + "delete": "删除队列", + "copyId": "复制队列Id", + "idCopiedMessage": "队列Id复制到剪贴板", + "description": "描述", + "description-hint": "该文本将显示在队列描述中,而不是选定的策略中。", + "alt-description": "提交策略: {{submitStrategy}},处理策略: {{processingStrategy}}", + "strategies": { + "sequential-by-originator-label": "设备顺序", + "sequential-by-originator-hint": "在未确认设备A的消息之前,设备A不会提交新的消息", + "sequential-by-tenant-label": "租户顺序", + "sequential-by-tenant-hint": "在未确认租户A的消息之前,租户A不会提交的新消息", + "sequential-label": "顺序", + "sequential-hint": "在未确认之前的消息,不提交新的消息", + "burst-label": "Burst", + "burst-hint": "所有消息都按照其到达的顺序提交给规则链", + "batch-label": "批量", + "batch-hint": "在未确认之前,不提交新批次", + "skip-all-failures-label": "跳过所有失败", + "skip-all-failures-hint": "忽略所有失败", + "skip-all-failures-and-timeouts-label": "跳过所有失败和超时", + "skip-all-failures-and-timeouts-hint": "忽略所有失败和超时", + "retry-all-label": "重试全部", + "retry-all-hint": "重试处理包中所有的消息", + "retry-failed-label": "重试失败", + "retry-failed-hint": "重试处理包中所有失败的消息", + "retry-timeout-label": "重试超时", + "retry-timeout-hint": "重试处理包中所有的定时消息", + "retry-failed-and-timeout-label": "重试失败和超时", + "retry-failed-and-timeout-hint": "重试处理包中所有失败和定时的消息" + } + }, + "server-error": { + "general": "一般服务器错误", + "authentication": "授权错误", + "jwt-token-expired": "JWT令牌已过期", + "tenant-trial-expired": "租户过期", + "credentials-expired": "凭据过期", + "permission-denied": "没有权限", + "invalid-arguments": "无效参数", + "bad-request-params": "请求无效", + "item-not-found": "找不到项目", + "too-many-requests": "请求过于频繁", + "too-many-updates": "更新过于频繁" + }, + "tenant": { + "tenant": "租户", + "tenants": "租户", + "management": "租户管理", + "add": "添加租户", + "admins": "管理员", + "manage-tenant-admins": "管理租户管理员", + "delete": "删除租户", + "add-tenant-text": "添加新租户", + "no-tenants-text": "没有找到租户", + "tenant-details": "租客详情", + "title-max-length": "标题长度应小于256", + "delete-tenant-title": "你确定要删除租户'{{tenantTitle}}'吗?", + "delete-tenant-text": "请注意:确认删除后租户数据将无法恢复。", + "delete-tenants-title": "确定要删除{count, plural,1 {1个租户} other {#个租户} }吗?", + "delete-tenants-action-title": "删除{ count, plural, 1 {1个租户} other {#个租户} }", + "delete-tenants-text": "请注意:确认删除后租户数据将无法恢复。", + "title": "标题", + "title-required": "标题必填。", + "description": "描述", + "details": "详情", + "events": "事件", + "copyId": "复制租户ID", + "idCopiedMessage": "租户ID已经复制到粘贴板", + "select-tenant": "选择租户", + "no-tenants-matching": "没有找到符合'{{entity}}'的租户", + "tenant-required": "租户必填", + "search": "查找租户", + "selected-tenants": "已选择{ count, plural, 1 {1个租户} other {#个租户} }", + "isolated-tb-rule-engine": "在独立的ThingsBoard规则引擎容器中处理", + "isolated-tb-rule-engine-details": "每个独立租户需要单独的微服务" + }, + "tenant-profile": { + "tenant-profile": "租户简介", + "tenant-profiles": "租户配置", + "add": "添加租户配置", + "edit": "编辑租户配置", + "tenant-profile-details": "租户配置详细信息", + "no-tenant-profiles-text": "找不到租户配置", + "name-max-length": "名称长度不能大于256", + "search": "查找租户配置", + "selected-tenant-profiles": "已选择{ count, plural, 1 {1个租户配置} other {#个租户配置} }", + "no-tenant-profiles-matching": "找不到与'{{entity}}'匹配的租户配置。", + "tenant-profile-required": "租户配置必填", + "idCopiedMessage": "租户配置Id已复制到剪贴板", + "set-default": "设置该租户配置为默认", + "delete": "删除租户配置", + "copyId": "复制租户配置Id", + "name": "名称", + "name-required": "名称必填。", + "data": "纵断面数据", + "profile-configuration": "配置设置", + "description": "描述", + "default": "默认", + "delete-tenant-profile-title": "确实要删除租户配置'{{tenantProfileName}}'吗?", + "delete-tenant-profile-text": "请注意:确认删除后租户配置和所有相关数据将不可。", + "delete-tenant-profiles-title": "确定要删除{ count, plural, 1 {1个租户配置} other {#个租户配置} }?", + "delete-tenant-profiles-text": "请注意:确认删除后户配置数据将无法恢复。", + "set-default-tenant-profile-title": "确实要将租户配置'{{tenantProfileName}}'设为默认值吗?", + "set-default-tenant-profile-text": "确认后此租户配置将被标记为默认配置,并将用于未指定配置的新租户。", + "no-tenant-profiles-found": "找不到租户配置。", + "create-new-tenant-profile": "创建一个新的!", + "create-tenant-profile": "创建新的租户配置文件", + "import": "导入租户配置文件", + "export": "导出租户配置文件", + "export-failed-error": "导出错误: {{error}}", + "tenant-profile-file": "租户配置文件", + "invalid-tenant-profile-file-error": "导入租户配置文件失败", + "advanced-settings": "高级设置", + "entities": "实体", + "rule-engine": "规则引擎", + "time-to-live": "过期时间", + "alarms-and-notifications": "警报和通知", + "ota-files-in-bytes": "OTA字节文件", + "ws-title": "WS", + "unlimited": "(0-无限)", + "maximum-devices": "最大设备数(0-无限制)", + "maximum-devices-required": "最大设备数必填。", + "maximum-devices-range": "最小设备数不能为负数", + "maximum-assets": "最大资产数量(0-无限)", + "maximum-assets-required": "最大资产数必填。", + "maximum-assets-range": "最大资产数不能为负数", + "maximum-customers": "最大客户数(0-无限制)", + "maximum-customers-required": "最大客户数必填。", + "maximum-customers-range": "最大客户数不能为负数", + "maximum-users": "最大用户数(0-无限制)", + "maximum-users-required": "最大用户数必填。", + "maximum-users-range": "最大用户数不能为负数", + "maximum-dashboards": "仪表板的最大数量(0-无限制)", + "maximum-dashboards-required": "最大仪表板数必填。", + "maximum-dashboards-range": "仪表板的最大数量不能为负数", + "maximum-rule-chains": "最大规则链数(0-无限制)", + "maximum-rule-chains-required": "最大规则链数必填。", + "maximum-rule-chains-range": "最大规则链数不能为负数", + "maximum-resources-sum-data-size": "资源文件大小总和", + "maximum-resources-sum-data-size-required": "资源文件大小总和必填。", + "maximum-resources-sum-data-size-range": "资源文件总和可能为负", + "maximum-ota-packages-sum-data-size": "OTA包文件大小总和(字节)", + "maximum-ota-package-sum-data-size-required": "OTA包文件大小总和(字节)必填。", + "maximum-ota-package-sum-data-size-range": "OTA包文件大小总和范围", + "transport-tenant-msg-rate-limit": "传输租户消息速率限制", + "transport-tenant-telemetry-msg-rate-limit": "租户遥测消息速率限制", + "transport-tenant-telemetry-data-points-rate-limit": "租户遥测数据点速率限制", + "transport-device-msg-rate-limit": "传输设备消息速率限制", + "transport-device-telemetry-msg-rate-limit": "设备遥测消息速率限制", + "transport-device-telemetry-data-points-rate-limit": "设备遥测数据点速率限制。", + "tenant-entity-export-rate-limit": "实体版本创建", + "tenant-entity-import-rate-limit": "实体版本加载", + "max-transport-messages": "最大传输消息数(0-无限制)", + "max-transport-messages-required": "最大传输消息数必填。", + "max-transport-messages-range": "传输消息的最大数目不能为负数", + "max-transport-data-points": "传输数据点的最大数量(0-无限制)", + "max-transport-data-points-required": "最大传输数据点数必填。", + "max-transport-data-points-range": "传输数据点的最大数目不能为负", + "max-r-e-executions": "最大规则引擎执行数(0-不受限制)", + "max-r-e-executions-required": "最大规则引擎执行数必填。", + "max-r-e-executions-range": "规则引擎执行的最大数目不能为负", + "max-j-s-executions": "最大JavaScript执行数(0-不受限制)", + "max-j-s-executions-required": "最大JavaScript执行数必填。", + "max-j-s-executions-range": "JavaScript的最大执行数不能为负数", + "max-d-p-storage-days": "最大日存储数据点数(0-无限制)", + "max-d-p-storage-days-required": "最大日存储数据点数必填。", + "max-d-p-storage-days-range": "最大日存储数据点数不能为负数", + "default-storage-ttl-days": "默认存储 TTL天数(0-无限制)", + "default-storage-ttl-days-required": "默认存储TTL天数必填。", + "default-storage-ttl-days-range": "不能为负存储天数", + "alarms-ttl-days": "警报超时(天)", + "alarms-ttl-days-required": "警报超时必填", + "alarms-ttl-days-days-range": "警报超时(天)非负数", + "rpc-ttl-days": "RPC超时(天)", + "rpc-ttl-days-required": "RPC超时(天)必填", + "rpc-ttl-days-days-range": "RPC超时(天)非负数", + "max-rule-node-executions-per-message": "每条消息的最大规则节点执行数(0-无限制)", + "max-rule-node-executions-per-message-required": "每个消息的最大规则节点执行数必填。", + "max-rule-node-executions-per-message-range": "每条消息的最大规则节点执行数不能为负", + "max-emails": "发送的最大电子邮件数(0-无限制)", + "max-emails-required": "最大电子邮件发送数必填。", + "max-emails-range": "发送的最大电子邮件数不能为负数", + "max-sms": "发送的最大短信数(0-无限制)", + "max-sms-required": "最大短信发送数必填。", + "max-sms-range": "发送的最大短信数不能为负数", + "max-created-alarms": "警报最大值", + "max-created-alarms-required": "警报最大值必填。", + "max-created-alarms-range": "警报最大值非负数", + "no-queue": "未配置队列", + "add-queue": "添加队列", + "queues-with-count": "队列({{count}})", + "tenant-rest-limits": "租户REST请求", + "customer-rest-limits": "客户REST请求", + "incorrect-pattern-for-rate-limits": "此格式以逗号分隔的分区和速率(以秒为单位) 例如:100:1,2000:60", + "too-small-value-zero": "值必须大于0", + "too-small-value-one": "值必须大于1", + "cassandra-tenant-limits-configuration": "租户查询(Cassandra)", + "ws-limit-max-sessions-per-tenant": "租户最大会话数量", + "ws-limit-max-sessions-per-customer": "客户最大会话数量", + "ws-limit-max-sessions-per-regular-user": "用户最大会话数量", + "ws-limit-max-sessions-per-public-user": "公开用户最大会话数量", + "ws-limit-queue-per-session": "消息队列最大会话数量", + "ws-limit-max-subscriptions-per-tenant": "租户订阅最大数量", + "ws-limit-max-subscriptions-per-customer": "客户订阅最大数量", + "ws-limit-max-subscriptions-per-regular-user": "用户订阅最大数量", + "ws-limit-max-subscriptions-per-public-user": "公开用户订阅最大数量", + "ws-limit-updates-per-session": "WS会话更新", + "rate-limits": { + "add-limit": "Add limit", + "advanced-settings": "Advanced settings", + "edit-limit": "Edit limit", + "but-less-than": "but less than", + "edit-transport-tenant-msg-title": "Edit transport tenant messages rate limits", + "edit-transport-tenant-telemetry-msg-title": "Edit transport tenant telemetry messages rate limits", + "edit-transport-tenant-telemetry-data-points-title": "Edit transport tenant telemetry data points rate limits", + "edit-transport-device-msg-title": "Edit transport device messages rate limits", + "edit-transport-device-telemetry-msg-title": "Edit transport device telemetry messages rate limits", + "edit-transport-device-telemetry-data-points-title": "Edit transport device telemetry data points rate limits", + "edit-transport-tenant-msg-rate-limit-title": "Edit transport tenant messages rate limits", + "edit-customer-rest-limits-title": "Edit REST requests for customer rate limits", + "edit-ws-limit-updates-per-session-title": "Edit WS updates per session rate limits", + "edit-cassandra-tenant-limits-configuration-title": "Edit Cassandra query for tenant rate limits", + "edit-tenant-entity-export-rate-limit-title": "Edit entity version creation rate limits", + "edit-tenant-entity-import-rate-limit-title": "Edit entity version load rate limits", + "messages-per": "messages per", + "not-set": "Not set", + "number-of-messages": "Number of messages", + "number-of-messages-required": "Number of messages is required.", + "number-of-messages-min": "Minimum value is 1.", + "preview": "Preview", + "per-seconds": "Per seconds", + "per-seconds-required": "Time rate is required.", + "per-seconds-min": "Minimum value is 1.", + "rate-limits": "Rate limits", + "remove-limit": "Remove limit", + "transport-tenant-msg": "Transport tenant messages", + "transport-tenant-telemetry-msg": "Transport tenant telemetry messages", + "transport-tenant-telemetry-data-points": "Transport tenant telemetry data points", + "transport-device-msg": "Transport device messages", + "transport-device-telemetry-msg": "Transport device telemetry messages", + "transport-device-telemetry-data-points": "Transport device telemetry data points", + "sec": "sec" + } + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {#秒} other {#秒} }", + "minutes-interval": "{ minutes, plural, 1 {#分} other {#分} }", + "hours-interval": "{ hours, plural, 1 {#小时} other {#小时} }", + "days-interval": "{ days, plural, 1 {#天} other {#天} }", + "days": "天", + "hours": "小时", + "minutes": "分钟", + "seconds": "秒", + "advanced": "高级", + "predefined": { + "yesterday": "昨天", + "day-before-yesterday": "前天", + "this-day-last-week": "上周今天", + "previous-week": "上周(周日至周六)", + "previous-week-iso": "上周(周一至周日)", + "previous-month": "上月", + "previous-year": "去年", + "current-hour": "当时", + "current-day": "今天", + "current-day-so-far": "今天当前", + "current-week": "本周(周日至周六)", + "current-week-iso": "本周(周一至周日)", + "current-week-so-far": "本周(周至周六)", + "current-week-iso-so-far": "本周(周一至周日)", + "current-month": "本月", + "current-month-so-far": "本月当前", + "current-year": "今年", + "current-year-so-far": "今年当前" + } + }, + "timeunit": { + "milliseconds": "毫秒", + "seconds": "秒", + "minutes": "分钟", + "hours": "小时", + "days": "天" + }, + "timewindow": { + "days": "{ days, plural, 1 {#天 } other {#天 } }", + "hours": "{ hours, plural, 0 {-小时 } 1 {#小时 } other {#小时 } }", + "minutes": "{ minutes, plural, 0 {-分 } 1 {#分 } other {#分 } }", + "seconds": "{ seconds, plural, 0 {-秒 } 1 {#秒 } other {#秒 } }", + "realtime": "实时", + "history": "历史", + "last-prefix": "最后", + "period": "从{{ startTime }}到{{ endTime }}", + "edit": "编辑时间窗口", + "date-range": "日期范围", + "last": "最后", + "time-period": "时间范围", + "hide": "隐藏", + "interval": "时间间隔" + }, + "user": { + "user": "用户", + "users": "用户", + "customer-users": "客户用户", + "tenant-admins": "租户管理员", + "sys-admin": "系统管理员", + "tenant-admin": "租户管理员", + "customer": "客户", + "anonymous": "匿名", + "add": "添加用户", + "delete": "删除用户", + "add-user-text": "添加新用户", + "no-users-text": "找不到用户", + "user-details": "用户详细信息", + "delete-user-title": "你确定要删除用户'{{userEmail}}'吗?", + "delete-user-text": "请注意:确认删除后用户数据将无法恢复。", + "delete-users-title": "确定要删除 { count, plural, 1 {1个用户} other {#个用户} } 吗?", + "delete-users-action-title": "删除 { count, plural, 1 {1个用户} other {#个用户} }", + "delete-users-text": "请注意:确认删除后用户数据将无法恢复。", + "activation-email-sent-message": "激活电子邮件已成功发送!", + "resend-activation": "重新发送激活", + "email": "电子邮件", + "email-required": "电子邮件必填。", + "invalid-email-format": "无效的邮件格式。", + "first-name": "名字", + "last-name": "姓", + "description": "描述", + "default-dashboard": "默认面板", + "always-fullscreen": "始终全屏", + "select-user": "选择用户", + "no-users-matching": "没有找到符合'{{entity}}'的用户。", + "user-required": "用户必填", + "activation-method": "激活方式", + "display-activation-link": "显示激活链接", + "send-activation-mail": "发送激活邮件", + "activation-link": "用户激活链接", + "activation-link-text": "使用该链接激活 激活用户:", + "copy-activation-link": "复制用户激活链接", + "activation-link-copied-message": "用户激活链接已经复制到粘贴板", + "details": "详情", + "login-as-tenant-admin": "以租户管理员身份登录", + "login-as-customer-user": "以客户用户身份登录", + "search": "查找用户", + "selected-users": "已选择{count, plural, 1 {1个用户} other {#个用户} }", + "disable-account": "禁用用户帐户", + "enable-account": "启用用户帐户", + "enable-account-message": "已成功启用用户帐户!", + "disable-account-message": "已成功禁用用户帐户!", + "copyId": "复制用户Id", + "idCopiedMessage": "用户ID已经复制到粘贴板" + }, + "value": { + "type": "值类型", + "string": "字符串", + "string-value": "字符串值", + "string-value-required": "字符串值必填", + "integer": "数字", + "integer-value": "数字值", + "integer-value-required": "整数值必填", + "invalid-integer-value": "整数值无效", + "double": "双精度小数", + "double-value": "双精度小数值", + "double-value-required": "需要双精度值", + "boolean": "布尔值", + "boolean-value": "布尔值", + "false": "假", + "true": "真", + "long": "Long", + "json": "JSON", + "json-value": "JSON值", + "json-value-invalid": "JSON值的格式无效", + "json-value-required": "JSON值必填。" + }, + "version-control": { + "version-control": "版本控制", + "management": "版本控制管理", + "search": "搜索版本", + "branch": "分支", + "default": "默认", + "select-branch": "选择版本", + "branch-required": "分支必填", + "create-entity-version": "创建实体版本", + "version-name": "版本名称", + "version-name-required": "版本名称必填", + "author": "作者", + "export-relations": "导出关系", + "export-attributes": "导出属性", + "export-credentials": "导出任证", + "entity-versions": "实体版本", + "versions": "版本", + "created-time": "创建时间", + "version-id": "版本ID", + "no-entity-versions-text": "没有找到实体版本", + "no-versions-text": "没有找到版本", + "copy-full-version-id": "复制全部版本ID", + "create-version": "创建版本", + "creating-version": "正在创建版本请等待. . .", + "nothing-to-commit": "没有更改的变化", + "restore-version": "还原版本", + "restore-entity-from-version": "从版'{{versionName}}恢复实体'", + "restoring-entity-version": "正在恢复版本请等待. . .", + "load-relations": "加载关系", + "load-attributes": "加载属性", + "load-credentials": "加载凭证", + "compare-with-current": "与当前进行比较", + "diff-entity-with-version": "与实体版本'{{versionName}}'", + "previous-difference": "以前差异", + "next-difference": "下一个差异", + "current": "当前", + "differences": "{ count, plural, 1 {1处差异} other {#处差异} }", + "create-entities-version": "创建实体版本", + "default-sync-strategy": "默认同步策略", + "sync-strategy-merge": "合并", + "sync-strategy-overwrite": "覆盖", + "entities-to-export": "实体导出", + "entities-to-restore": "实体恢复", + "sync-strategy": "同步策略", + "all-entities": "所有实体", + "no-entities-to-export-prompt": "请指定导出的实体", + "no-entities-to-restore-prompt": "请指定要恢复的实体", + "add-entity-type": "添加实体类型", + "remove-all": "移除全部", + "version-create-result": "{ added, plural, 0 {没有实体} 1 {1实体} other {#实体} }添加
{ modified, plural, 0 {没有实体} 1 {1实体} other {#实体} }修改。
{ removed, plural, 0 {没有实体} 1 {1实体} other {#实体} }移除。", + "remove-other-entities": "移除其它实体", + "find-existing-entity-by-name": "根据名称查找存在实体", + "restore-entities-from-version": "从版本'{{versionName}}'恢复实体", + "restoring-entities-from-version": "正在从版本恢复实体请等待. . .", + "no-entities-restored": "没有恢复实体", + "created": "{{created}}创建", + "updated": "{{updated}}更新", + "deleted": "{{deleted}}删除", + "remove-other-entities-confirm-text": "请注意:将永久删除你要恢复的版本中不存在所有当前实体
请输入要移除其它实体进行确认。", + "auto-commit-to-branch": "自动提交{{ branch }}分支", + "default-create-entity-version-name": "{{entityName}}更新", + "sync-strategy-merge-hint": "在仓库中创建或更新选中实体,其他仓库实体不会被修改。", + "sync-strategy-overwrite-hint": "在仓库中创建或更新选中实体,其他仓库实体同步被删除。", + "device-credentials-conflict": "无法载外部id为{{entityId}}的设备
因为已经有相同的数据存在数据库中。
请考虑禁用还原窗口中的加载凭据设置。", + "missing-referenced-entity": "未能加载外部id为{{sourceEntityTypeName}}和{{sourceEntityId}}
因为引用缺少{{targetEntityTypeName}}和b>{{targetEntityId}}。", + "runtime-failed": "失败:{{message}}", + "auto-commit-settings-read-only-hint": "自动提交功能不适用于存储库设置中启用的只读选项。" + }, + "widget": { + "widget-library": "部件包", + "widget-bundle": "部件包", + "all-bundles": "所有部件包", + "select-widgets-bundle": "选择部件包", + "management": "管理部件", + "editor": "部件编辑器", + "widget-type-not-found": "加载部件配置出错。
可能关联的部件已经删除了。", + "widget-type-load-error": "由于以下错误未加载小部件:", + "remove": "删除部件", + "edit": "编辑部件", + "remove-widget-title": "确实要删除'{{widgetTitle}}'部件吗?", + "remove-widget-text": "确认删除后数据将无法恢复。", + "timeseries": "时间序列", + "search-data": "查找数据", + "no-data-found": "没有找到数据", + "latest": "最新值", + "rpc": "控件部件", + "alarm": "警报部件", + "static": "静态部件", + "select-widget-type": "选择窗口部件类型", + "missing-widget-title-error": "部件标题必须指定!", + "widget-saved": "部件已保存", + "unable-to-save-widget-error": "数据错误部件无法保存!", + "save": "保存部件", + "saveAs": "部件另存为", + "save-widget-type-as": "部件类型另存为", + "save-widget-type-as-text": "请输入新的部件标题或选择目标部件包", + "toggle-fullscreen": "切换全屏", + "run": "运行部件", + "title": "部件标题", + "title-required": "部件标题必填。", + "type": "部件类型", + "resources": "资源", + "resource-url": "JavaScript/CSS URL", + "resource-is-module": "是否模块", + "remove-resource": "删除资源", + "add-resource": "添加资源", + "html": "HTML", + "tidy": "对齐", + "css": "CSS", + "settings-schema": "设置模式", + "datakey-settings-schema": "数据键设置模式", + "latest-datakey-settings-schema": "最新数据键设置模式", + "widget-settings": "部件设置", + "description": "描述", + "image-preview": "图片预览", + "settings-form-selector": "设置表单选择器", + "data-key-settings-form-selector": "数据键设置表单选择器", + "latest-data-key-settings-form-selector": "最新数据键设置表单选择器", + "javascript": "Javascript", + "js": "JS", + "remove-widget-type-title": "你确定要删除部件类型'{{widgetName}}'吗?", + "remove-widget-type-text": "确认删除后部件类型数据将无法恢复。", + "remove-widget-type": "删除部件类型", + "add-widget-type": "添加新的部件类型", + "widget-type-load-failed-error": "无法加载部件类型!", + "widget-template-load-failed-error": "无法加载部件模板!", + "add": "添加部件", + "undo": "撤消部件更改", + "export": "导出部件", + "no-data": "小部件上没有要显示的数据", + "data-overflow": "部件显示 {{count}}之外{{total}} 实体", + "alarm-data-overflow": "窗口部件显示{{allowedEntities}}(最大允许的)实的警报{{totalEntities}}实体", + "search": "搜索部件", + "filter": "部件类型过滤", + "loading-widgets": "加载部件..." + }, + "widget-action": { + "header-button": "顶部按钮clcik", + "open-dashboard-state": "切换到新仪表板状态", + "update-dashboard-state": "更新当前仪表板状态", + "open-dashboard": "切换到另一个仪表板", + "custom": "自定义动作", + "custom-pretty": "自定义操作(使用HTML模板)", + "mobile-action": "移动端动作", + "target-dashboard-state": "目标仪表板状态", + "target-dashboard-state-required": "目标仪表板状态必填", + "set-entity-from-widget": "从部件中设置实体", + "target-dashboard": "目标仪表板", + "open-right-layout": "打开右侧布局 (移动端视图)", + "state-display-type": "显示仪表板状态选项", + "open-normal": "普通", + "open-in-separate-dialog": "在单独的对话框中打开", + "open-in-popover": "在popover打开", + "dialog-title": "对话框标题", + "dialog-hide-dashboard-toolbar": "在对话框中隐藏仪表板工具栏", + "dialog-width": "对话框宽度相对视图宽度", + "dialog-height": "对话框高度相对视图高度", + "dialog-size-range-error": "对话框尺寸比值必须是1至100范围内。", + "popover-preferred-placement": "首选弹出位置", + "popover-placement-top": "上", + "popover-placement-topLeft": "左上", + "popover-placement-topRight": "右上", + "popover-placement-right": "右", + "popover-placement-rightTop": "右上", + "popover-placement-rightBottom": "右下", + "popover-placement-bottom": "下", + "popover-placement-bottomLeft": "左下", + "popover-placement-bottomRight": "右下", + "popover-placement-left": "左", + "popover-placement-leftTop": "左上", + "popover-placement-leftBottom": "左下", + "popover-hide-on-click-outside": "点击隐藏外面的弹出窗口", + "popover-hide-dashboard-toolbar": "隐藏仪表板工具栏", + "popover-width": "浏览器中的弹出宽度(例如100px, 25vw)单位", + "popover-height": "浏览器中的弹出高度(例如:100px,25vh)单位", + "popover-style": "弹出样式", + "open-new-browser-tab": "在新的浏览器选项卡中打开", + "mobile": { + "action-type": "手机端动作类型", + "action-type-required": "手机端动作类型必填。", + "take-picture-from-gallery": "画册拍照", + "take-photo": "拍照", + "map-direction": "打开地图说明", + "map-location": "打开地图位置", + "scan-qr-code": "扫描二维码", + "make-phone-call": "拨打电话", + "get-location": "获得位置", + "take-screenshot": "截图" + } + }, + "widgets-bundle": { + "current": "当前组", + "widgets-bundles": "部件包", + "add": "添加部件包", + "delete": "删除部件包", + "title": "标题", + "title-required": "标题必填。", + "title-max-length": "标题长度应小于256", + "description": "描述", + "image-preview": "图片预览", + "add-widgets-bundle-text": "添加新的部件包", + "no-widgets-bundles-text": "找不到部件包", + "empty": "部件包是空的", + "details": "详情", + "widgets-bundle-details": "部件包详细信息", + "delete-widgets-bundle-title": "你确定要删除部件包'{{widgetsBundleTitle}}'吗?", + "delete-widgets-bundle-text": "请注意:确认删除后部件包数据将无法恢复。", + "delete-widgets-bundles-title": "确定要删除{ count, plural, 1 {1个部件包} other {#个部件包} }吗?", + "delete-widgets-bundles-action-title": "删除{ count, plural, 1 {1个部件包} other {#个部件包} }", + "delete-widgets-bundles-text": "请注意:确认删除后部件包数据将无法恢复。", + "no-widgets-bundles-matching": "没有找到与'{{widgetsBundle}}'匹配的部件包。", + "widgets-bundle-required": "部件包必填。", + "system": "系统", + "import": "导入部件包", + "export": "导出部件包", + "export-failed-error": "无法导出部件包: {{error}}", + "create-new-widgets-bundle": "创建新的部件包", + "widgets-bundle-file": "部件包文件", + "invalid-widgets-bundle-file-error": "部件包数据结构错误无法导入。", + "search": "查找部件包", + "selected-widgets-bundles": "已选择{ count, plural, 1 {1个部件包} other {#个部件包} }", + "open-widgets-bundle": "打开部件包", + "loading-widgets-bundles": "加载部件包..." + }, + "widget-config": { + "data": "数据", + "settings": "设置", + "advanced": "高级", + "title": "标题", + "title-tooltip": "标题提示框", + "general-settings": "基础设置", + "display-title": "显示标题", + "drop-shadow": "阴影", + "enable-fullscreen": "启用全屏", + "background-color": "背景颜色", + "text-color": "文字颜色", + "padding": "内边距", + "margin": "外边距", + "widget-style": "部件风格", + "widget-css": "部件CSS", + "title-style": "标题风格", + "mobile-mode-settings": "移动端设置", + "order": "顺序", + "height": "高度", + "mobile-hide": "移动端隐藏部件", + "units": "特殊符号展示值", + "decimals": "浮点数后的位数", + "timewindow": "时间窗口", + "use-dashboard-timewindow": "使用仪表板的时间窗口", + "display-timewindow": "显示时间窗口", + "legend": "图例", + "display-legend": "显示图例", + "datasources": "数据源", + "maximum-datasources": "最大允许{ count, plural, 1 {1个数据源。} other {#个数据源。} }", + "timeseries-key-error": "应至少指定一个时间序列数据密钥", + "datasource-type": "类型", + "datasource-parameters": "参数", + "remove-datasource": "移除数据源", + "add-datasource": "添加数据源", + "target-device": "目标设备", + "alarm-source": "警报源", + "actions": "动作", + "action": "动作", + "add-action": "添加动作", + "search-actions": "搜索动作", + "no-actions-text": "找不到动作", + "action-source": "动作源", + "action-source-required": "动作源必填", + "action-name": "名称", + "action-name-required": "动作名称必填。", + "action-name-not-unique": "动作名称已经存在。
相同动作源的动作名称必须唯一。", + "action-icon": "图标", + "show-hide-action-using-function": "使用函数显示/隐藏操作", + "action-type": "类型", + "action-type-required": "类型必填", + "edit-action": "编辑动作", + "delete-action": "删除删除", + "delete-action-title": "删除部件动作", + "delete-action-text": "确定要删除部件动作'{{actionName}}'吗?", + "title-icon": "标题图标", + "display-icon": "显示标题图标", + "icon-color": "图标颜色", + "icon-size": "图标大小", + "advanced-settings": "高级设置", + "data-settings": "数据设置", + "no-data-display-message": "\"无数据显示\" 替代消息", + "data-page-size": "每个数据源的最大实体数", + "settings-component-not-found": "找不到选择器'{{selector}}'的每个数据源设置窗体组件的最大实体数" + }, + "widget-type": { + "import": "导入部件类型", + "export": "导出部件类型", + "export-failed-error": "无法导出部件类型: {{error}}", + "create-new-widget-type": "创建新的部件类型", + "widget-type-file": "部件类型文件", + "invalid-widget-type-file-error": "部件类型数据结构错误无法导入。" + }, + "widgets": { + "chart": { + "common-settings": "通用设置", + "enable-stacking-mode": "启用堆叠模式", + "line-shadow-size": "线条阴影大小", + "display-smooth-lines": "显示光滑(弯曲)线条", + "default-bar-width": "默认条形宽度不聚合数据(毫秒)", + "bar-alignment": "条形对齐", + "bar-alignment-left": "左", + "bar-alignment-right": "右", + "bar-alignment-center": "中", + "default-font-size": "默认字体大小", + "default-font-color": "默认字体颜色", + "thresholds-line-width": "所有线条默认宽度", + "tooltip-settings": "提示栏设置", + "show-tooltip": "显示提示栏", + "hover-individual-points": "坐标悬停显示", + "show-cumulative-values": "在堆叠模式下显示累加数据", + "hide-zero-false-values": "隐藏提示栏中0或false值", + "tooltip-value-format-function": "提示栏格式化函数", + "grid-settings": "网格设置", + "show-vertical-lines": "显示垂直参考线", + "show-horizontal-lines": "显示水平参考线", + "grid-outline-border-width": "网格边框宽度(像系)", + "primary-color": "边框颜色", + "background-color": "背景颜色", + "ticks-color": "参考线颜色", + "xaxis-settings": "X轴设置", + "axis-title": "标题", + "xaxis-tick-labels-settings": "标签设置", + "show-tick-labels": "显示标签", + "yaxis-settings": "y轴设置", + "min-scale-value": "最小值", + "max-scale-value": "最大值", + "yaxis-tick-labels-settings": "标签设置", + "tick-step-size": "步长", + "number-of-decimals": "小数位数", + "ticks-formatter-function": "刻度格式化函数", + "comparison-settings": "比较设置", + "enable-comparison": "启用比较", + "time-for-comparison": "比较时期", + "time-for-comparison-previous-interval": "历史时间间隔(默认)", + "time-for-comparison-days": "一天前", + "time-for-comparison-weeks": "一周前", + "time-for-comparison-months": "一月前", + "time-for-comparison-years": "一年前", + "time-for-comparison-custom-interval": "自定义间隔", + "custom-interval-value": "自定义间隔值(毫秒)", + "comparison-x-axis-settings": "比较x轴设置", + "axis-position": "位置", + "axis-position-top": "上面(默认)", + "axis-position-bottom": "下面", + "custom-legend-settings": "自定义图例设置", + "enable-custom-legend": "启用自定义图例(你可以使用属性/时间序列标签)", + "key-name": "键名", + "key-name-required": "键名必填", + "key-type": "键类型", + "key-type-attribute": "属性", + "key-type-timeseries": "时间序列", + "label-keys-list": "在标签中使用键列表", + "no-label-keys": "没有配置键", + "add-label-key": "添加键", + "line-width": "线条宽度", + "color": "颜色", + "data-is-hidden-by-default": "默认隐藏数据", + "disable-data-hiding": "禁用用数据隐藏", + "remove-from-legend": "从图例中移除数据键", + "exclude-from-stacking": "从堆叠模式排除", + "line-settings": "线条设置", + "show-line": "显示线条", + "fill-line": "线条填充", + "points-settings": "点设置", + "show-points": "显示点", + "points-line-width": "宽度", + "points-radius": "圆角", + "point-shape": "形状", + "point-shape-circle": "圆形", + "point-shape-cross": "十字形", + "point-shape-diamond": "菱形", + "point-shape-square": "矩形", + "point-shape-triangle": "三角形", + "point-shape-custom": "自定义", + "point-shape-draw-function": "绘制函数", + "show-separate-axis": "显示独立轴", + "axis-position-left": "左", + "axis-position-right": "右", + "thresholds": "阈值", + "no-thresholds": "没有配置阈值", + "add-threshold": "添加阈值", + "show-values-for-comparison": "显示比较的历史值", + "comparison-values-label": "历史数什标签", + "threshold-settings": "阈值设置", + "use-as-threshold": "将值作为阈值", + "threshold-line-width": "阈值线条宽度", + "threshold-color": "阈值颜色", + "common-pie-settings": "饼图设置", + "radius": "圆角", + "inner-radius": "内圆角", + "tilt": "倾斜", + "stroke-settings": "斜线设置", + "width-pixels": "宽度(像素)", + "show-labels": "显示标签", + "animation-settings": "动画设置", + "animated-pie": "启用动画", + "border-settings": "边框设置", + "border-width": "边框宽度", + "border-color": "边框颜色", + "legend-settings": "图例设置", + "display-legend": "显示图例", + "labels-font-color": "标签字体颜色" + }, + "dashboard-state": { + "dashboard-state-settings": "仪表板状态设置", + "dashboard-state": "仪表板状态ID", + "autofill-state-layout": "自动填充状态布局高度", + "default-margin": "部件默认外边距", + "default-background-color": "默认背景色", + "sync-parent-state-params": "同步父仪表板状态参数" + }, + "date-range-navigator": { + "date-range-picker-settings": "日期范围选择设置", + "hide-date-range-picker": "隐藏日期范围选择", + "picker-one-panel": "日期范围选择板", + "picker-auto-confirm": "自动确认日期范围选择", + "picker-show-template": "日期范围选择显示模板", + "first-day-of-week": "一周的第一天", + "interval-settings": "间隔设置", + "hide-interval": "隐藏间隔", + "initial-interval": "初始间隔", + "interval-hour": "小时", + "interval-day": "天", + "interval-week": "周", + "interval-two-weeks": "2周", + "interval-month": "月", + "interval-three-months": "3个月", + "interval-six-months": "6个月", + "step-settings": "步长设置", + "hide-step-size": "步长设置", + "initial-step-size": "初始步长大小", + "hide-labels": "隐藏标签", + "use-session-storage": "使用会话存储", + "localizationMap": { + "Sun": "周日", + "Mon": "周一", + "Tue": "周二", + "Wed": "周三", + "Thu": "周四", + "Fri": "周五", + "Sat": "周六", + "Jan": "1月", + "Feb": "2月", + "Mar": "3月", + "Apr": "4月", + "May": "5月", + "Jun": "6月", + "Jul": "7月", + "Aug": "8月", + "Sep": "9月", + "Oct": "10月", + "Nov": "11月", + "Dec": "12月", + "January": "一月", + "February": "二月", + "March": "三月", + "April": "四月", + "June": "六月", + "July": "七月", + "August": "八月", + "September": "九月", + "October": "十月", + "November": "十一月", + "December": "十二月", + "Custom Date Range": "自定义日期范围", + "Date Range Template": "日期范围模板", + "Today": "今天", + "Yesterday": "昨天", + "This Week": "本星期", + "Last Week": "上星期", + "This Month": "本月", + "Last Month": "上月", + "Year": "年", + "This Year": "今年", + "Last Year": "去年", + "Date picker": "日期选择", + "Hour": "小时", + "Day": "天", + "Week": "周", + "2 weeks": "2周", + "Month": "月", + "3 months": "3个月", + "6 months": "6个月", + "Custom interval": "自定义间隔", + "Interval": "间隔", + "Step size": "步长", + "Ok": "确定" + } + }, + "entities-hierarchy": { + "hierarchy-data-settings": "层次数据设置", + "relations-query-function": "关系查询函数", + "has-children-function": "是否子级函数", + "node-state-settings": "状态设置", + "node-opened-function": "展开函数", + "node-disabled-function": "禁用函数", + "display-settings": "显示设置", + "node-icon-function": "icon函数", + "node-text-function": "文本函数", + "sort-settings": "排序设置", + "nodes-sort-function": "排序函数" + }, + "edge": { + "display-default-title": "显示默认标题" + }, + "gateway": { + "general-settings": "基础设置", + "widget-title": "部件村题", + "default-archive-file-name": "默认文件名称", + "device-type-for-new-gateway": "网关设备类型", + "messages-settings": "消息设置", + "save-config-success-message": "配置保存成功消息", + "device-name-exists-message": "设备名称已经存在消消息", + "gateway-title": "网关标题", + "read-only": "只读", + "events-title": "事件标题", + "events-filter": "事件过滤", + "event-key-contains": "包含事件key..." + }, + "gauge": { + "default-color": "默认颜色", + "radial-gauge-settings": "仪表盘设置", + "ticks-settings": "刻度设置", + "min-value": "最小值", + "max-value": "最大值", + "start-ticks-angle": "起始角度", + "ticks-angle": "结束角度", + "major-ticks-count": "主要刻度数量", + "major-ticks-color": "主要刻度颜色", + "minor-ticks-count": "次要刻度数量", + "minor-ticks-color": "次要刻度颜色", + "tick-numbers-font": "字体", + "unit-title-settings": "标题设置", + "show-unit-title": "显示标题", + "unit-title": "单位", + "title-font": "字体", + "units-settings": "单位设置", + "units-font": "字体", + "value-box-settings": "数值设置", + "show-value-box": "显示数值框", + "value-int": "长度", + "value-font": "字体", + "value-box-rect-stroke-color": "数值框矩形颜色", + "value-box-rect-stroke-color-end": "数值框矩形颜色渐变", + "value-box-background-color": "数值框背景色", + "value-box-shadow-color": "数值框阴影颜色", + "plate-settings": "表盘设置", + "show-plate-border": "显示边框", + "plate-color": "背景色", + "needle-settings": "指针设置", + "needle-circle-size": "针圈大小", + "needle-color": "颜色", + "needle-color-end": "渐变色", + "needle-color-shadow-up": "针头颜色", + "needle-color-shadow-down": "阴影颜色", + "highlights-settings": "色块设置", + "highlights-width": "色块宽度", + "highlights": "色块", + "highlight-from": "从", + "highlight-to": "到", + "highlight-color": "颜色", + "no-highlights": "没有配置色块", + "add-highlight": "添加色块", + "animation-settings": "动画设置", + "enable-animation": "启用动画", + "animation-duration": "动画时长", + "animation-rule": "动画类型", + "animation-linear": "线形", + "animation-quad": "方形", + "animation-quint": "五角形", + "animation-cycle": "环形", + "animation-bounce": "循环", + "animation-elastic": "弹跳", + "animation-dequad": "Dequad", + "animation-dequint": "Dequint", + "animation-decycle": "Decycle", + "animation-debounce": "Debounce", + "animation-delastic": "Delastic", + "linear-gauge-settings": "线型设置", + "bar-stroke-width": "量规宽度", + "bar-stroke-color": "量规颜色", + "bar-background-color": "量规背景色", + "bar-background-color-end": "量规背景渐变色", + "progress-bar-color": "进度条颜色", + "progress-bar-color-end": "进度条渐变色", + "major-ticks-names": "主要刻度名称", + "show-stroke-ticks": "显示主要刻度", + "major-ticks-font": "主要刻度字体", + "border-color": "边框颜色", + "border-width": "边框宽度", + "needle-circle-color": "指针颜色", + "animation-target": "动画触发", + "animation-target-needle": "指针", + "animation-target-plate": "表盘", + "common-settings": "通用设置", + "gauge-type": "仪表盘类型", + "gauge-type-arc": "弧形", + "gauge-type-donut": "环形", + "gauge-type-horizontal-bar": "水平", + "gauge-type-vertical-bar": "垂直", + "donut-start-angle": "角度从", + "bar-settings": "条形设置", + "relative-bar-width": "相对条宽", + "neon-glow-brightness": "霓虹灯效果亮度,(0-100),0-禁用效果", + "stripes-thickness": "条纹的厚度0是没有条纹", + "rounded-line-cap": "显示圆形线盖", + "bar-color-settings": "条颜色设置", + "use-precise-level-color-values": "使用精确的颜色值", + "bar-colors": "从下到上的条形颜色", + "color": "颜色", + "no-bar-colors": "没有配置条形颜色", + "add-bar-color": "添加条形颜色", + "from": "从", + "to": "到", + "fixed-level-colors": "连界值条形颜色", + "gauge-title-settings": "仪表盘标题设置", + "show-gauge-title": "显示仪表盘标题", + "gauge-title": "仪表盘标题", + "gauge-title-font": "仪表盘标题", + "unit-title-and-timestamp-settings": "单位标题和时间戳设置", + "show-timestamp": "显示值时间戳", + "timestamp-format": "时间戳格式化", + "label-font": "标签字体显示值", + "value-settings": "数值设置", + "show-value": "显示文本值", + "min-max-settings": "最小/最大标签设置", + "show-min-max": "显示最小值和最大值", + "min-max-font": "标签最大和最小字体", + "show-ticks": "显示刻度", + "tick-width": "刻度宽度", + "tick-color": "刻度颜色", + "tick-values": "刻度值", + "no-tick-values": "没有配置刻度值", + "add-tick-value": "添加刻度值" + }, + "gpio": { + "pin": "Pin", + "label": "标签", + "row": "行", + "column": "列", + "color": "颜色", + "panel-settings": "面板设置", + "background-color": "背景颜色", + "gpio-switches": "GPIO开关", + "no-gpio-switches": "没有配置GPIO开关", + "add-gpio-switch": "添加开关", + "gpio-status-request": "GPIO状态请求", + "method-name": "方法名", + "method-body": "方法体", + "gpio-status-change-request": "GPIO状态更改请求", + "parse-gpio-status-function": "GPIO状态解析函数", + "gpio-leds": "led", + "no-gpio-leds": "没有配置led", + "add-gpio-led": "添加LED" + }, + "html-card": { + "html": "HTML", + "css": "CSS" + }, + "input-widgets": { + "attribute-not-allowed": "属性参数不能在此小部件中使用", + "blocked-location": "在浏览器中阻止地理位置", + "claim-device": "声明设备", + "claim-failed": "声明设备失败!", + "claim-not-found": "找不到设备!", + "claim-successful": "设备已成功申领!", + "date": "日期", + "device-name": "设备名称", + "device-name-required": "设备名称必填", + "discard-changes": "放弃更改", + "entity-attribute-required": "实体属性必填", + "entity-coordinate-required": "纬度和经度两个字段都是必需的", + "entity-timeseries-required": "实体时间序列必填", + "get-location": "获取当前位置", + "invalid-date": "无效日期", + "latitude": "纬度", + "longitude": "经度", + "min-value-error": "最小值是{{value}}", + "max-value-error": "最大值是{{value}}", + "not-allowed-entity": "所选实体不能具有共享属性", + "no-attribute-selected": "未选择任何属性", + "no-datakey-selected": "未选择数据键", + "no-coordinate-specified": "未指定纬度/经度的数据键", + "no-entity-selected": "未选择实体", + "no-image": "没有图像", + "no-support-geolocation": "你的浏览器不支持地理定位", + "no-support-web-camera": "你的浏览器不支持摄像头", + "enable-https-use-widget": "请启用HTTPS以使用此部件", + "no-found-your-camera": "找不到摄像机", + "no-permission-camera": "权限被用户拒绝/此站点无权使用摄像机", + "no-timeseries-selected": "未选择时间序列", + "secret-key": "密钥", + "secret-key-required": "密钥必填", + "switch-attribute-value": "切换实体属性值", + "switch-camera": "切换摄像机", + "switch-timeseries-value": "切换实体时间序列值", + "take-photo": "拍照", + "time": "时间", + "timeseries-not-allowed": "时间序列参数不能用于此部件", + "update-failed": "更新失败", + "update-successful": "更新成功", + "update-attribute": "更新属性", + "update-timeseries": "更新时间序列", + "value": "数据值", + "general-settings": "基础设置", + "widget-title": "部件标题", + "claim-button-label": "声称按钮标签", + "show-secret-key-field": "显示“密钥”输入字段", + "labels-settings": "标签设置", + "show-labels": "显示标签", + "device-name-label": "设备名称设备名称输入字段的标签输入字段", + "secret-key-label": "设备名称设备密钥输入字段的标签输入字段", + "messages-settings": "信息设置", + "claim-device-success-message": "设备声明成功的文本信息", + "claim-device-not-found-message": "设备声明未找到信息", + "claim-device-failed-message": "设备声明失败的文本信息", + "claim-device-name-required-message": "设备声明“设备名称”必填的错误信息", + "claim-device-secret-key-required-message": "设备声明“设备密钥”必填的错误信息", + "show-label": "显示标签", + "label": "标签", + "required": "必填", + "required-error-message": "错误信息“必填”", + "show-result-message": "显示结果信息", + "integer-field-settings": "Integer字段设置", + "min-value": "最大值", + "max-value": "最小值", + "double-field-settings": "Double字段设置", + "text-field-settings": "Text字段设置", + "min-length": "最小长度", + "max-length": "最大长度", + "checkbox-settings": "Checkbox设置", + "true-label": "Checked标签", + "false-label": "Unchecked标签", + "image-input-settings": "Image字段设置", + "display-preview": "显示预览", + "display-clear-button": "显示清除按钮", + "display-apply-button": "显示应用按钮", + "display-discard-button": "显示弃用按钮", + "datetime-field-settings": "Date/time字段设置", + "display-time-input": "显示time输入", + "latitude-key-name": "Latitude键名", + "longitude-key-name": "Longitude键名", + "show-get-location-button": "显示“获取当前位置”按钮", + "use-high-accuracy": "使用高精度", + "location-fields-settings": "位置设置", + "latitude-label": "latitude标签", + "longitude-label": "longitude标签", + "input-fields-alignment": "对齐方式", + "input-fields-alignment-column": "列(默认)", + "input-fields-alignment-row": "行", + "latitude-field-required": "Latitude字段必填", + "longitude-field-required": "Longitude字段必填", + "attribute-settings": "属性设置", + "widget-mode": "部件模式", + "widget-mode-update-attribute": "更新属性", + "widget-mode-update-timeseries": "更新时间序列", + "attribute-scope": "属性范围", + "attribute-scope-server": "服务端属性", + "attribute-scope-shared": "共享属性", + "value-required": "启用数值必填", + "image-settings": "图片设置", + "image-format": "图片格式", + "image-format-jpeg": "JPEG", + "image-format-png": "PNG", + "image-format-webp": "WEBP", + "image-quality": "图片压缩", + "max-image-width": "最大宽度", + "max-image-height": "最大高度", + "action-buttons": "动作按钮", + "show-action-buttons": "显示动作按钮", + "update-all-values": "更新所有值", + "save-button-label": "“保存”按钮标签", + "reset-button-label": "“撤销”按钮标签", + "group-settings": "分组设置", + "show-group-title": "显示分组标题", + "group-title": "分组标题", + "fields-alignment": "对齐字段", + "fields-alignment-row": "行(默认)", + "fields-alignment-column": "列", + "fields-in-row": "行中的字段数", + "option-value": "值(为创建空选项编写“null”)", + "option-label": "标签", + "hide-input-field": "隐藏输入字段", + "datakey-type": "Datakey类型", + "datakey-type-server": "服务端属性(默认)", + "datakey-type-shared": "共享属性", + "datakey-type-timeseries": "时间序列", + "datakey-value-type": "Datakey值类型", + "datakey-value-type-string": "String", + "datakey-value-type-double": "Double", + "datakey-value-type-integer": "Integer", + "datakey-value-type-boolean-checkbox": "Boolean(多选)", + "datakey-value-type-boolean-switch": "Boolean(开关)", + "datakey-value-type-date-time": "Date & Time", + "datakey-value-type-date": "Date", + "datakey-value-type-time": "Time", + "datakey-value-type-select": "Select", + "value-is-required": "值是必填", + "ability-to-edit-attribute": "编辑属性的能力", + "ability-to-edit-attribute-editable": "可编辑(默认)", + "ability-to-edit-attribute-disabled": "禁用", + "ability-to-edit-attribute-readonly": "只读", + "disable-on-datakey-name": "禁用另一个datakey的错误值(指定datakey名称)", + "slide-toggle-settings": "滑块设置", + "slide-toggle-label-position": "滑动标签位置", + "slide-toggle-label-position-after": "后面", + "slide-toggle-label-position-before": "前面", + "select-options": "下拉选项", + "no-select-options": "没有配置下拉选项", + "add-select-option": "添加下接选项", + "numeric-field-settings": "数字字段设置", + "step-interval": "步长间隔", + "error-messages": "错误信息", + "min-value-error-message": "“最小值”错误信息", + "max-value-error-message": "“最大值”错误信息", + "invalid-date-error-message": "“无效日期”错误消息", + "icon-settings": "图标设置", + "use-custom-icon": "自定义图标", + "input-cell-icon": "输入单元格之前显示的图标", + "value-conversion-settings": "值转换设置", + "get-value-settings": "获取值设置", + "use-get-value-function": "启用获取值函数", + "get-value-function": "获取值函数", + "set-value-settings": "设置值设置", + "use-set-value-function": "启用值值函数", + "set-value-function": "启用函数" + }, + "invalid-qr-code-text": "无效的二维码文本。", + "qr-code": { + "use-qr-code-text-function": "启用二维码文本函数", + "qr-code-text-pattern": "二维码文本模式(例如:${entityName}|${keyName}一些文本。')", + "qr-code-text-pattern-hint": "二维码文本模式使用实体别名中第一个找到的键的值。", + "qr-code-text-pattern-required": "二维码文本必填。", + "qr-code-text-function": "二维码文本函数" + }, + "label-widget": { + "label-pattern": "模式", + "label-pattern-hint": "提示文本,如:'${keyName}单位。'或${#<key index>}单位'", + "label-pattern-required": "模式必填", + "label-position": "相对位置", + "x-pos": "X", + "y-pos": "Y", + "background-color": "背景色", + "font-settings": "字体设置", + "background-image": "背景图片", + "labels": "标签", + "no-labels": "没有配置标签", + "add-label": "添加标签" + }, + "navigation": { + "title": "标题", + "navigation-path": "导航路径", + "filter-type": "过滤类型", + "filter-type-all": "所有内容", + "filter-type-include": "包含内容", + "filter-type-exclude": "排除内容", + "items": "内容", + "enter-urls-to-filter": "输入URL进行过滤..." + }, + "persistent-table": { + "rpc-id": "RPC ID", + "message-type": "消息类型", + "method": "方法", + "params": "参数", + "created-time": "创建时间", + "expiration-time": "到期时间", + "retries": "重试", + "status": "状态", + "filter": "过滤", + "refresh": "刷新", + "add": "添加RPC请求", + "details": "详情", + "delete": "删除", + "delete-request-title": "删除持续化RPC请求", + "delete-request-text": "您确定要删除请求吗?", + "details-title": "详情RPC ID: ", + "additional-info": "附加信息", + "response": "响应", + "any-status": "任何状态", + "rpc-status-list": "RPC状态列表", + "no-request-prompt": "无显示请求", + "send-request": "发送请求", + "add-title": "创建持续化RPC请求", + "method-error": "方法名必填。", + "timeout-error": "最小超时值为5000(5秒)。", + "white-space-error": "不允许使用空白。", + "rpc-status": { + "QUEUED": "QUEUED", + "SENT": "SENT", + "DELIVERED": "DELIVERED", + "SUCCESSFUL": "SUCCESSFUL", + "TIMEOUT": "TIMEOUT", + "EXPIRED": "EXPIRED", + "FAILED": "FAILED" + }, + "rpc-search-status-all": "所有", + "message-types": { + "false": "Two-way", + "true": "One-way" + }, + "general-settings": "基础设置", + "enable-filter": "启用过滤", + "enable-sticky-header": "滚动时显示标头", + "enable-sticky-action": "滚动时显示动作列", + "display-request-details": "显示请求详情", + "allow-send-request": "允许发送RPC请求", + "allow-delete-request": "允许删除请求", + "columns-settings": "设置列", + "display-columns": "显示列", + "column": "列", + "no-columns-found": "没有配置列", + "no-columns-matching": "'{{column}}'找不到。" + }, + "rpc": { + "value-settings": "设置值", + "initial-value": "初始值", + "retrieve-value-settings": "恢复开/关设置", + "retrieve-value-method": "恢复方法", + "retrieve-value-method-none": "无需恢复", + "retrieve-value-method-rpc": "RPC方法恢复", + "retrieve-value-method-attribute": "订阅属性", + "retrieve-value-method-timeseries": "订阅时间序列", + "attribute-value-key": "属性key", + "timeseries-value-key": "时序key", + "get-value-method": "RPC获取方法", + "parse-value-function": "解析函数", + "update-value-settings": "更新值设置", + "set-value-method": "RPC获取方法", + "convert-value-function": "转换函数", + "rpc-settings": "RPC设置", + "request-timeout": "RPC请求超(毫秒)", + "persistent-rpc-settings": "持久化RPC设置", + "request-persistent": "RPC请求持久化", + "persistent-polling-interval": " RPC持久化轮询间隔(毫秒)", + "common-settings": "通用设置", + "switch-title": "开关标题", + "show-on-off-labels": "显示开/关", + "slide-toggle-label": "滑块切换标签", + "label-position": "标签位置", + "label-position-before": "前面", + "label-position-after": "后面", + "slider-color": "滑块颜色", + "slider-color-primary": "Primary", + "slider-color-accent": "Accent", + "slider-color-warn": "Warn", + "button-style": "按钮样式", + "button-raised": "按钮凸起", + "button-primary": "原色", + "button-background-color": "背景色", + "button-text-color": "文字颜色", + "widget-title": "部件标题", + "button-label": "按钮标签", + "device-attribute-scope": "设备属性范围", + "server-attribute": "服务端属性", + "shared-attribute": "共享属性", + "device-attribute-parameters": "设备属性参数", + "is-one-way-command": "是否单向命令", + "rpc-method": "RPC方法", + "rpc-method-params": "RPC参数", + "show-rpc-error": "显示RPC命令执行错误", + "led-title": "LED标题", + "led-color": "LED颜色", + "check-status-settings": "检查状态设置", + "perform-rpc-status-check": "执行RPC设备状态检查", + "retrieve-led-status-value-method": "使用方法状态值恢复LED", + "led-status-value-attribute": "设备属性包含LED状态值", + "led-status-value-timeseries": "设备时间序列包含LED状态值", + "check-status-method": "RPC设备状态检查方法", + "parse-led-status-value-function": "解析函数", + "knob-title": "旋转按钮", + "min-value": "最小值", + "max-value": "最大值" + }, + "maps": { + "select-entity": "选择实体", + "select-entity-hint": "提示:选择后单击地图以设置位置", + "tooltips": { + "placeMarker": "单击放置'{{entityName}}'实体", + "firstVertex": "单击放置多边形'{{entityName}}'的第一个点", + "firstVertex-cut": "单击放置第一个点", + "continueLine": "单击继续绘制'{{entityName}}'多边形", + "continueLine-cut": "单击继续绘制", + "finishLine": "单击任何现有标记完成", + "finishPoly": "单击第一个标记完成'{{entityName}}'并保存", + "finishPoly-cut": "单击第一个标记完成并保存", + "finishRect": "单击'{{entityName}}'多边形完成并保存", + "startCircle": "单击'{{entityName}}'放置圆圈中心", + "finishCircle": "单击'{{entityName}}'完成圆圈", + "placeCircleMarker": "单击放置圆形标记" + }, + "actions": { + "finish": "完成", + "cancel": "取消", + "removeLastVertex": "删除最后一点" + }, + "buttonTitles": { + "drawMarkerButton": "放置实体", + "drawPolyButton": "创建多边形", + "drawLineButton": "创建多边形线条", + "drawCircleButton": "创建圆形", + "drawRectButton": "创建矩形", + "editButton": "编辑模式", + "dragButton": "拖放模式", + "cutButton": "剪切多边形区域", + "deleteButton": "移除", + "drawCircleMarkerButton": "创建圆形标志", + "rotateButton": "旋转多边形" + }, + "map-provider-settings": "地图设置", + "map-provider": "提供程序", + "map-provider-google": "Google地图", + "map-provider-openstreet": "OpenStreet地图", + "map-provider-here": "HERE地图", + "map-provider-image": "Image地图", + "map-provider-tencent": "Tencent地图", + "openstreet-provider": "OpenStreet地图提供程序", + "openstreet-provider-mapnik": "OpenStreetMap.Mapnik(默认)", + "openstreet-provider-hot": "OpenStreetMap.HOT", + "openstreet-provider-esri-street": "Esri.WorldStreetMap", + "openstreet-provider-esri-topo": "Esri.WorldTopoMap", + "openstreet-provider-esri-imagery": "Esri.WorldImagery", + "openstreet-provider-cartodb-positron": "CartoDB.Positron", + "openstreet-provider-cartodb-dark-matter": "CartoDB.DarkMatter", + "use-custom-provider": "自定义提供程序", + "custom-provider-tile-url": "提供程序URL", + "google-maps-api-key": "Google地图Key", + "default-map-type": "默认地图类型", + "google-map-type-roadmap": "路线地图", + "google-map-type-satelite": "卫星", + "google-map-type-hybrid": "混合", + "google-map-type-terrain": "街道", + "map-layer": "地图", + "here-map-normal-day": "HERE.normalDay(默认)", + "here-map-normal-night": "HERE.normalNight", + "here-map-hybrid-day": "HERE.hybridDay", + "here-map-terrain-day": "HERE.terrainDay", + "credentials": "证书", + "here-app-id": "HERE地图id", + "here-app-code": "HERE地图code", + "tencent-maps-api-key": "Tencent地图Key", + "tencent-map-type-roadmap": "路线地图", + "tencent-map-type-satelite": "卫星", + "tencent-map-type-hybrid": "混合", + "image-map-background": "地图北景", + "image-map-background-from-entity-attribute": "从实体属性中获取背景图", + "image-url-source-entity-alias": "图像URL源实体别名", + "image-url-source-entity-attribute": "图像URL源实体属性", + "common-map-settings": "通用设置", + "x-pos-key-name": "X位置键名", + "y-pos-key-name": "Y位置键名", + "latitude-key-name": "Latitude键名", + "longitude-key-name": "Longitude键名", + "default-map-zoom-level": "地图缩放级别(1-20)", + "default-map-center-position": "地图中心位置(0,0)", + "disable-scroll-zooming": "地图中心位置", + "disable-double-click-zooming": "禁用双击缩放", + "disable-zoom-control-buttons": "禁用缩放按钮", + "fit-map-bounds": "拟合地图标记", + "use-default-map-center-position": "地图默认中心位置", + "entities-limit": "加载实体限制", + "markers-settings": "标记设置", + "marker-offset-x": "标记X宽度偏移量", + "marker-offset-y": "标记Y高度偏移量", + "position-function": "位置转换函数应返回x,y坐标", + "draggable-marker": "拖动标记", + "label": "标签设置", + "show-label": "显示标签", + "use-label-function": "标签函数", + "label-pattern": "标签模式(例如:'${entityName}', '${entityName}: (文本${keyName}单位)' )", + "label-function": "标签函数", + "tooltip": "提示栏设置", + "show-tooltip": "显示提示栏", + "show-tooltip-action": "提示拦动作", + "show-tooltip-action-click": "单击显示(默认)", + "show-tooltip-action-hover": "悬停显示", + "auto-close-tooltips": "自动关闭", + "use-tooltip-function": "启用函数", + "tooltip-pattern": "提示栏(例如:'文本${keyName}单位。'或 Link text')", + "tooltip-function": "提示栏函数", + "tooltip-offset-x": "提示栏x标记宽度偏移量", + "tooltip-offset-y": "提示栏y标记高度偏移量", + "color": "颜色设置", + "use-color-function": "启用函数", + "color-function": "颜色函数", + "marker-image": "图片标记设置", + "use-marker-image-function": "启用函数", + "custom-marker-image": "自定义图片标记", + "custom-marker-image-size": "自定义图片标记大小(像素)", + "marker-image-function": "自定义图片标记函数", + "marker-images": "图片标记", + "polygon-settings": "多边形设置", + "show-polygon": "启用多边形", + "polygon-key-name": "多边形键名", + "enable-polygon-edit": "启用编辑", + "polygon-label": "标签", + "show-polygon-label": "启用标签", + "use-polygon-label-function": "启用函数", + "polygon-label-pattern": "标签模式(例如: '${entityName}', '${entityName}:(文本${keyName}单位)' )", + "polygon-label-function": "标签函数", + "polygon-tooltip": "提示栏", + "show-polygon-tooltip": "启用提示栏", + "auto-close-polygon-tooltips": "自动关闭", + "use-polygon-tooltip-function": "启用函数", + "polygon-tooltip-pattern": "提示栏(例如:'文本${keyName}单位'或Link text')", + "polygon-tooltip-function": "提示栏函数", + "polygon-color": "颜色", + "polygon-opacity": "透明度”", + "use-polygon-color-function": "启用函数", + "polygon-color-function": "颜色函数", + "polygon-stroke": "斜线", + "stroke-color": "斜线颜色", + "stroke-opacity": "透明度", + "stroke-weight": "宽度", + "use-polygon-stroke-color-function": "启用函数", + "polygon-stroke-color-function": "颜色函数", + "circle-settings": "圆形设置", + "show-circle": "启用圆形", + "circle-key-name": "圆形键名", + "enable-circle-edit": "启用编辑", + "circle-label": "标签", + "show-circle-label": "启用标签", + "use-circle-label-function": "启用函数", + "circle-label-pattern": "标签模式(例如:'${entityName}', '${entityName}:(文本${keyName}单位)')", + "circle-label-function": "标签函数", + "circle-tooltip": "提示栏", + "show-circle-tooltip": "显示提示栏", + "auto-close-circle-tooltips": "自动关闭", + "use-circle-tooltip-function": "启用函数", + "circle-tooltip-pattern": "提示栏(例如:'文本${keyName}单位。'或Link text')", + "circle-tooltip-function": "提示栏函数", + "circle-fill-color": "填充色", + "circle-fill-color-opacity": "透明度", + "use-circle-fill-color-function": "启用函数", + "circle-fill-color-function": "填充颜色函数", + "circle-stroke": "斜线设置", + "use-circle-stroke-color-function": "启用函数", + "circle-stroke-color-function": "颜色函数", + "markers-clustering-settings": "标记集群设置", + "use-map-markers-clustering": "启用地图标记集群", + "zoom-on-cluster-click": "单击群集时变焦", + "max-cluster-zoom": "集群最大的缩放比例(0-18)", + "max-cluster-radius-pixels": "最大半径", + "cluster-zoom-animation": "放大动画", + "show-markers-bounds-on-cluster-mouse-over": "显示鼠标在群集上时显示标记的边界", + "spiderfy-max-zoom-level": "最大缩放比例(查看所有群集标记)", + "load-optimization": "负载优化", + "cluster-chunked-loading": "块加载集群标记", + "cluster-markers-lazy-load": "延迟加载集群标记", + "editor-settings": "编辑设置", + "enable-snapping": "启用顶点捕捉", + "init-draggable-mode": "启用拖动模式初始化地图", + "hide-all-edit-buttons": "隐藏所有按钮", + "hide-draw-buttons": "隐藏绘制按钮", + "hide-edit-buttons": "隐藏编辑按钮", + "hide-remove-button": "隐藏删除按钮", + "route-map-settings": "路由设置", + "trip-animation-settings": "旅行动画设置", + "normalization-step": "正常数据步长(ms)", + "tooltip-background-color": "工具提示背景颜色", + "tooltip-font-color": "工具提示字体颜色", + "tooltip-opacity": "工具提示不透明度(0-1)", + "auto-close-tooltip": "自动关闭工具提示", + "rotation-angle": "设置标记的附加旋转角度(度)", + "path-settings": "路径设置", + "path-color": "路径颜色", + "use-path-color-function": "使用路径颜色功能", + "path-color-function": "路径颜色功能", + "path-decorator": "路径装饰器", + "use-path-decorator": "使用路径装饰器", + "decorator-symbol": "装饰符号", + "decorator-symbol-arrow-head": "箭头", + "decorator-symbol-dash": "短跑", + "decorator-symbol-size": "装饰器符号尺寸(像素)", + "use-path-decorator-custom-color": "使用路径装饰器自定义颜色", + "decorator-custom-color": "装饰器定制颜色", + "decorator-offset": "装饰器偏移", + "end-decorator-offset": "结束装饰器偏移", + "decorator-repeat": "装饰器重复", + "points-settings": "点设置", + "show-points": "显示点", + "point-color": "点颜色", + "point-size": "点大小(像素)", + "use-point-color-function": "使用点颜色功能", + "point-color-function": "点颜色函数", + "use-point-as-anchor": "使用点作为锚", + "point-as-anchor-function": "点为锚函数", + "independent-point-tooltip": "独立点工具提示", + "clustering-markers": "集群标记", + "use-icon-create-function": "启用集群标记", + "marker-color-function": "标记颜色函数" + }, + "markdown": { + "use-markdown-text-function": "使用markdown/HTML函数", + "markdown-text-function": "Markdown/HTML函数", + "markdown-text-pattern": "Markdown/HTML模式(markdown或HTML变量, 例如:'${entityName}或${keyName}一些文本')", + "markdown-css": "Markdown/HTML CSS" + }, + "simple-card": { + "label-position": "标签位置", + "label-position-left": "左", + "label-position-top": "上" + }, + "table": { + "common-table-settings": "表格设置", + "enable-search": "启用搜索", + "enable-sticky-header": "始终显示标题", + "enable-sticky-action": "始终显示动作", + "hidden-cell-button-display-mode": "隐藏单元按钮操作显示模式", + "show-empty-space-hidden-action": "显示空空间而不是隐藏的单元按钮动作", + "dont-reserve-space-hidden-action": "不要为隐藏动作按钮保留空间", + "display-timestamp": "显示时间", + "display-milliseconds": "显示毫秒", + "display-pagination": "显示分页", + "default-page-size": "分页数量", + "use-entity-label-tab-name": "实体标签名称", + "hide-empty-lines": "隐藏空线", + "row-style": "行样式", + "use-row-style-function": "启用样式函数", + "row-style-function": "样式函数", + "cell-style": "列样式", + "use-cell-style-function": "启用样式函数", + "cell-style-function": "样式函数", + "cell-content": "内容", + "use-cell-content-function": "启用内容函数", + "cell-content-function": "内容函数", + "show-latest-data-column": "显示最新数据列", + "latest-data-column-order": "样式函数", + "entities-table-title": "实体表格标题", + "enable-select-column-display": "启用数据列", + "display-entity-name": "显示实体名称", + "entity-name-column-title": "实体名称列标题", + "display-entity-label": "显示实体标签", + "entity-label-column-title": "实体标签列标题", + "display-entity-type": "显示实体类型", + "default-sort-order": "默认排序", + "custom-title": "Custom header title", + "column-width": "列宽", + "default-column-visibility": "默认显示", + "column-visibility-visible": "显示", + "column-visibility-hidden": "隐藏", + "column-visibility-hidden-mobile": "Hidden in mobile mode", + "column-selection-to-display": "“要显示”列中的列选择", + "column-selection-to-display-enabled": "启用", + "column-selection-to-display-disabled": "禁用", + "alarms-table-title": "警报标题", + "enable-alarms-selection": "启用警报选择", + "enable-alarms-search": "启用警报搜索", + "enable-alarm-filter": "启用警报过滤器", + "display-alarm-details": "显示警报详细", + "allow-alarms-ack": "允许确认警报", + "allow-alarms-clear": "允许清除警报" + }, + "value-source": { + "value-source": "数值源", + "predefined-value": "预定义值", + "entity-attribute": "从实体属性中获取值", + "value": "数据值", + "source-entity-alias": "实体别", + "source-entity-attribute": "实体属性" + }, + "widget-font": { + "font-family": "字体", + "size": "大小", + "relative-font-size": "相对字体大小", + "font-style": "样式", + "font-style-normal": "常规", + "font-style-italic": "斜体", + "font-style-oblique": "倾斜", + "font-weight": "字形", + "font-weight-normal": "常规", + "font-weight-bold": "加粗", + "font-weight-bolder": "相对加粗", + "font-weight-lighter": "相对常规", + "color": "颜色", + "shadow-color": "阴影颜色" + } + }, + "icon": { + "icon": "图标", + "select-icon": "选择图标", + "material-icons": "素材图标", + "show-all": "显示所有图标" + }, + "phone-input": { + "phone-input-label": "电话号码", + "phone-input-required": "电话号码必填。", + "phone-input-validation": "电话号码无效或不可用。", + "phone-input-pattern": "电话号码无效应该是E.164格式例如:{{phoneNumber}}", + "phone-input-hint": "电话号码无效应该是E.164格式例如:{{phoneNumber}}" + }, + "custom": { + "widget-action": { + "action-cell-button": "单元格click", + "row-click": "数据行click", + "polygon-click": "多边形click", + "marker-click": "标记click", + "circle-click": "圆形click", + "tooltip-tag-action": "提示框click", + "node-selected": "节点选中click", + "element-click": "HTML元素click", + "pie-slice-click": " 饼状click", + "row-double-click": "双击clcik" + } + }, + "paginator": { + "items-per-page": "每页数量:", + "first-page-label": "第一页", + "last-page-label": "最后一页", + "next-page-label": "下一页", + "previous-page-label": "上一页", + "items-per-page-separator": "-" + }, + "language": { + "language": "语言", + "locales": { + "de_DE": "德语", + "fr_FR": "法语", + "zh_CN": "简体中文", + "zh_TW": "繁體中文", + "en_US": "英语", + "it_IT": "意大利语", + "ko_KR": "韩语", + "ru_RU": "俄罗斯语", + "es_ES": "西班牙语", + "es_CA": "Catalan", + "ja_JP": "日本語", + "tr_TR": "土耳其语", + "fa_IR": "波斯语", + "uk_UA": "乌克兰语", + "cs_CZ": "捷克语", + "el_GR": "希腊语", + "ro_RO": "罗马尼亚语", + "lv_LV": "拉脱维亚语", + "ka_GE": "格鲁吉亚语", + "pt_BR": "葡萄牙语", + "sl_SI": "斯洛文尼亚语" + } } - } -} +} \ No newline at end of file From 0a9ad404b18c77b2434fb61aaa165a08640d1f78 Mon Sep 17 00:00:00 2001 From: zbeacon Date: Mon, 7 Nov 2022 12:27:09 +0200 Subject: [PATCH 040/226] CassandraBaseTimeseriesDao refactoring --- .../CassandraBaseTimeseriesDao.java | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index 7f8dca5af1..3d704a0429 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -645,14 +645,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD if (saveStmts == null) { var stmts = new PreparedStatement[DataType.values().length]; for (DataType type : DataType.values()) { - stmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF + - "(" + ModelConstants.ENTITY_TYPE_COLUMN + - "," + ModelConstants.ENTITY_ID_COLUMN + - "," + ModelConstants.KEY_COLUMN + - "," + ModelConstants.PARTITION_COLUMN + - "," + ModelConstants.TS_COLUMN + - "," + getColumnName(type) + ")" + - " VALUES(?, ?, ?, ?, ?, ?)"); + stmts[type.ordinal()] = prepare(getPreparedStatementQuery(type)); } saveStmts = stmts; } @@ -670,14 +663,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD if (saveTtlStmts == null) { var stmts = new PreparedStatement[DataType.values().length]; for (DataType type : DataType.values()) { - stmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF + - "(" + ModelConstants.ENTITY_TYPE_COLUMN + - "," + ModelConstants.ENTITY_ID_COLUMN + - "," + ModelConstants.KEY_COLUMN + - "," + ModelConstants.PARTITION_COLUMN + - "," + ModelConstants.TS_COLUMN + - "," + getColumnName(type) + ")" + - " VALUES(?, ?, ?, ?, ?, ?) USING TTL ?"); + stmts[type.ordinal()] = prepare(getPreparedStatementQueryWithTtl(type)); } saveTtlStmts = stmts; } @@ -688,6 +674,21 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD return saveTtlStmts[dataType.ordinal()]; } + private String getPreparedStatementQuery(DataType type) { + return INSERT_INTO + ModelConstants.TS_KV_CF + + "(" + ModelConstants.ENTITY_TYPE_COLUMN + + "," + ModelConstants.ENTITY_ID_COLUMN + + "," + ModelConstants.KEY_COLUMN + + "," + ModelConstants.PARTITION_COLUMN + + "," + ModelConstants.TS_COLUMN + + "," + getColumnName(type) + ")" + + " VALUES(?, ?, ?, ?, ?, ?)"; + } + + private String getPreparedStatementQueryWithTtl(DataType type) { + return getPreparedStatementQuery(type) + " USING TTL ?"; + } + private PreparedStatement getPartitionInsertStmt() { if (partitionInsertStmt == null) { stmtCreationLock.lock(); From 9b54f009e6a9c6e2b01714001c9b39a009ef7c20 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 7 Nov 2022 11:29:27 +0100 Subject: [PATCH 041/226] upgrade asset-profiles using ExecutorService --- .../install/DbUpgradeExecutorService.java | 26 ++++++++++++++++ .../install/SqlDatabaseUpgradeService.java | 30 +++++++++++-------- 2 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/install/DbUpgradeExecutorService.java diff --git a/application/src/main/java/org/thingsboard/server/service/install/DbUpgradeExecutorService.java b/application/src/main/java/org/thingsboard/server/service/install/DbUpgradeExecutorService.java new file mode 100644 index 0000000000..b3ce77ac7a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/DbUpgradeExecutorService.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2022 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.install; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; + +@Component +@Profile("install") +public class DbUpgradeExecutorService extends DbCallbackExecutorService { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 092726d0c8..668f20ce63 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.service.install; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -33,13 +35,11 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; import org.thingsboard.server.dao.asset.AssetProfileService; -import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.queue.QueueService; -import org.thingsboard.server.dao.rule.RuleChainService; -import org.thingsboard.server.dao.tenant.TenantProfileService; +import org.thingsboard.server.dao.sql.asset.AssetRepository; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration; @@ -56,7 +56,10 @@ import java.sql.SQLException; import java.sql.SQLSyntaxErrorException; import java.sql.SQLWarning; import java.sql.Statement; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO; @@ -110,7 +113,7 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService private DeviceService deviceService; @Autowired - private AssetService assetService; + private AssetRepository assetRepository; @Autowired private DeviceProfileService deviceProfileService; @@ -129,10 +132,7 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService private TbRuleEngineQueueConfigService queueConfig; @Autowired - private RuleChainService ruleChainService; - - @Autowired - private TenantProfileService tenantProfileService; + private DbUpgradeExecutorService dbUpgradeExecutor; @Override public void upgradeDatabase(String fromVersion) throws Exception { @@ -623,17 +623,20 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.info("Creating default asset profiles..."); PageLink pageLink = new PageLink(100); PageData pageData; + List> futures = new ArrayList<>(); do { pageData = tenantService.findTenants(pageLink); for (Tenant tenant : pageData.getData()) { - List assetTypes = assetService.findAssetTypesByTenantId(tenant.getId()).get(); + Set assetTypes = new HashSet<>(assetRepository.findTenantAssetTypes(tenant.getUuidId())); + assetTypes.remove("default"); + try { - assetProfileService.createDefaultAssetProfile(tenant.getId()); + futures.add(dbUpgradeExecutor.submit(() -> assetProfileService.createDefaultAssetProfile(tenant.getId()))); } catch (Exception e) { } - for (EntitySubtype assetType : assetTypes) { + for (String assetType : assetTypes) { try { - assetProfileService.findOrCreateAssetProfile(tenant.getId(), assetType.getType()); + futures.add(dbUpgradeExecutor.submit(() -> assetProfileService.findOrCreateAssetProfile(tenant.getId(), assetType))); } catch (Exception e) { } } @@ -641,6 +644,8 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService pageLink = pageLink.nextPageLink(); } while (pageData.hasNext()); + Futures.allAsList(futures).get(); + log.info("Updating asset profiles..."); conn.createStatement().execute("call update_asset_profiles()"); @@ -728,5 +733,4 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService return queue; } - } From a9dabe14ca191e2d0a92adf210d70d8bca409555 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 7 Nov 2022 11:51:09 +0100 Subject: [PATCH 042/226] refactored --- .../server/service/install/SqlDatabaseUpgradeService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 668f20ce63..4b6f1e1a52 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -57,9 +57,7 @@ import java.sql.SQLSyntaxErrorException; import java.sql.SQLWarning; import java.sql.Statement; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.concurrent.TimeUnit; import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO; @@ -627,7 +625,7 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService do { pageData = tenantService.findTenants(pageLink); for (Tenant tenant : pageData.getData()) { - Set assetTypes = new HashSet<>(assetRepository.findTenantAssetTypes(tenant.getUuidId())); + List assetTypes = assetRepository.findTenantAssetTypes(tenant.getUuidId()); assetTypes.remove("default"); try { From c5e1c5a1a51330d6d510685564b2f1beb7a50f7e Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 7 Nov 2022 13:19:17 +0200 Subject: [PATCH 043/226] test fixes, refactoring --- msa/black-box-tests/pom.xml | 4 -- .../server/msa/AbstractContainerTest.java | 1 - .../server/msa/TestProperties.java | 2 +- .../server/msa/TestRestClient.java | 62 ++++++++++++------- .../connectivity/MqttGatewayClientTest.java | 32 +++++++--- .../msa/prototypes/DevicePrototypes.java | 3 +- pom.xml | 28 +++++++++ 7 files changed, 93 insertions(+), 39 deletions(-) diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index 054b4ee5a8..9cd38c0c05 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -70,25 +70,21 @@ org.testng testng - 7.6.1 test org.assertj assertj-core - 3.23.1 test io.rest-assured rest-assured - 5.2.0 test org.hamcrest hamcrest-all - 1.3 test diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java index df622a7752..df1fe4a522 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java @@ -55,7 +55,6 @@ public abstract class AbstractContainerTest { @BeforeSuite public void beforeSuite() { - RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); if ("false".equals(System.getProperty("runLocal", "false"))) { containerTestSuite.start(); } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java index f50caa1aee..6e3a024cda 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java @@ -38,7 +38,7 @@ public class TestProperties { if (instance.isActive()) { return WSS_URL; } - return getProperty("tb.baseUrl"); + return getProperty("tb.wsUrl"); } private static String getProperty(String propertyName) { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index eaf5332323..0b0c4b89bc 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -16,13 +16,21 @@ package org.thingsboard.server.msa; import com.fasterxml.jackson.databind.JsonNode; +import io.restassured.RestAssured; +import io.restassured.builder.ResponseSpecBuilder; import io.restassured.common.mapper.TypeRef; +import io.restassured.config.HeaderConfig; +import io.restassured.config.HttpClientConfig; +import io.restassured.config.RestAssuredConfig; +import io.restassured.filter.log.RequestLoggingFilter; +import io.restassured.filter.log.ResponseLoggingFilter; import io.restassured.http.ContentType; import io.restassured.path.json.JsonPath; import io.restassured.response.ValidatableResponse; import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import org.hamcrest.Matchers; import org.springframework.http.HttpStatus; -import org.thingsboard.rest.client.RestClient; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; @@ -40,24 +48,33 @@ import java.util.List; import java.util.Map; import static io.restassured.RestAssured.given; +import static org.apache.http.params.CoreConnectionPNames.CONNECTION_TIMEOUT; +import static org.apache.http.params.CoreConnectionPNames.SO_TIMEOUT; import static org.hamcrest.Matchers.is; import static org.hamcrest.core.AnyOf.anyOf; import static org.thingsboard.server.common.data.StringUtils.isEmpty; -import static org.thingsboard.server.msa.AbstractContainerTest.getRequestFactoryForSelfSignedCert; public class TestRestClient { private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization"; private final String baseURL; private String token; private String refreshToken; - private RequestSpecification spec; + private RequestSpecification requestSpec; + private ResponseSpecification responseSpec; protected static final String ACTIVATE_TOKEN_REGEX = "/api/noauth/activate?activateToken="; public TestRestClient(String url) { + RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); + baseURL = url; - spec = given().baseUri(baseURL).contentType(ContentType.JSON); + requestSpec = given().baseUri(baseURL) + .contentType(ContentType.JSON) + .config(RestAssuredConfig.config() + .headerConfig(HeaderConfig.headerConfig() + .overwriteHeadersWithName(JWT_TOKEN_HEADER_PARAM))); + if (url.matches("^(https)://.*$")) { - spec.relaxedHTTPSValidation(); + requestSpec.relaxedHTTPSValidation(); } } @@ -70,12 +87,11 @@ public class TestRestClient { .getBody().jsonPath(); token = jsonPath.get("token"); refreshToken = jsonPath.get("refreshToken"); - spec.header(JWT_TOKEN_HEADER_PARAM, "Bearer " + token) - .contentType(ContentType.JSON); + requestSpec.header(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); } public Device postDevice(String accessToken, Device device) { - return given().spec(spec).body(device) + return given().spec(requestSpec).body(device) .pathParams("accessToken", accessToken) .post("/api/device?accessToken={accessToken}") .then() @@ -85,7 +101,7 @@ public class TestRestClient { } public ValidatableResponse getDeviceById(DeviceId deviceId, int statusCode) { - return given().spec(spec) + return given().spec(requestSpec) .pathParams("deviceId", deviceId.getId()) .get("/api/device/{deviceId}") .then() @@ -97,7 +113,7 @@ public class TestRestClient { .as(Device.class); } public DeviceCredentials getDeviceCredentialsByDeviceId(DeviceId deviceId) { - return given().spec(spec).get("/api/device/{deviceId}/credentials", deviceId.getId()) + return given().spec(requestSpec).get("/api/device/{deviceId}/credentials", deviceId.getId()) .then() .assertThat() .statusCode(HttpStatus.OK.value()) @@ -106,41 +122,41 @@ public class TestRestClient { } public ValidatableResponse postTelemetry(String credentialsId, JsonNode telemetry) { - return given().spec(spec).body(telemetry) + return given().spec(requestSpec).body(telemetry) .post("/api/v1/{credentialsId}/telemetry", credentialsId) .then() .statusCode(HttpStatus.OK.value()); } public ValidatableResponse deleteDevice(DeviceId deviceId) { - return given().spec(spec) + return given().spec(requestSpec) .delete("/api/device/{deviceId}", deviceId.getId()) .then() .statusCode(HttpStatus.OK.value()); } public ValidatableResponse deleteDeviceIfExists(DeviceId deviceId) { - return given().spec(spec) + return given().spec(requestSpec) .delete("/api/device/{deviceId}", deviceId.getId()) .then() .statusCode(anyOf(is(HttpStatus.OK.value()),is(HttpStatus.NOT_FOUND.value()))); } public ValidatableResponse postTelemetryAttribute(String entityType, DeviceId deviceId, String scope, JsonNode attribute) { - return given().spec(spec).body(attribute) + return given().spec(requestSpec).body(attribute) .post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityType, deviceId.getId(), scope) .then() .statusCode(HttpStatus.OK.value()); } public ValidatableResponse postAttribute(String accessToken, JsonNode attribute) { - return given().spec(spec).body(attribute) + return given().spec(requestSpec).body(attribute) .post("/api/v1/{accessToken}/attributes/", accessToken) .then() .statusCode(HttpStatus.OK.value()); } public JsonNode getAttributes(String accessToken, String clientKeys, String sharedKeys) { - return given().spec(spec) + return given().spec(requestSpec) .queryParam("clientKeys", clientKeys) .queryParam("sharedKeys", sharedKeys) .get("/api/v1/{accessToken}/attributes", accessToken) @@ -153,7 +169,7 @@ public class TestRestClient { public PageData getRuleChains(PageLink pageLink) { Map params = new HashMap<>(); addPageLinkToParam(params, pageLink); - return given().spec(spec).queryParams(params) + return given().spec(requestSpec).queryParams(params) .get("/api/ruleChains") .then() .statusCode(HttpStatus.OK.value()) @@ -162,7 +178,7 @@ public class TestRestClient { } public RuleChain postRootRuleChain(RuleChain ruleChain) { - return given().spec(spec) + return given().spec(requestSpec) .body(ruleChain) .post("/api/ruleChain") .then() @@ -172,7 +188,7 @@ public class TestRestClient { } public RuleChainMetaData postRuleChainMetadata(RuleChainMetaData ruleChainMetaData) { - return given().spec(spec) + return given().spec(requestSpec) .body(ruleChainMetaData) .post("/api/ruleChain/metadata") .then() @@ -182,14 +198,14 @@ public class TestRestClient { } public void setRootRuleChain(RuleChainId ruleChainId) { - given().spec(spec) + given().spec(requestSpec) .post("/api/ruleChain/{ruleChainId}/root", ruleChainId.getId()) .then() .statusCode(HttpStatus.OK.value()); } public void deleteRuleChain(RuleChainId ruleChainId) { - given().spec(spec) + given().spec(requestSpec) .delete("/api/ruleChain/{ruleChainId}", ruleChainId.getId()) .then() .statusCode(HttpStatus.OK.value()); @@ -224,7 +240,7 @@ public class TestRestClient { params.put("fromType", fromId.getEntityType().name()); params.put("relationTypeGroup", relationTypeGroup.name()); - return given().spec(spec) + return given().spec(requestSpec) .pathParams(params) .get("/api/relations?fromId={fromId}&fromType={fromType}&relationTypeGroup={relationTypeGroup}") .then() @@ -234,7 +250,7 @@ public class TestRestClient { } public JsonNode postServerSideRpc(DeviceId deviceId, JsonNode serverRpcPayload) { - return given().spec(spec) + return given().spec(requestSpec) .body(serverRpcPayload) .post("/api/rpc/twoway/{deviceId}", deviceId.getId()) .then() diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java index f844be8a27..54aec26421 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java @@ -16,6 +16,9 @@ package org.thingsboard.server.msa.connectivity; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -24,11 +27,15 @@ import io.netty.buffer.Unpooled; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttHandler; @@ -44,12 +51,10 @@ import org.thingsboard.server.msa.AbstractContainerTest; import org.thingsboard.server.msa.WsClient; import org.thingsboard.server.msa.mapper.WsTelemetryResponse; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.*; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.common.data.DataConstants.DEVICE; @@ -187,7 +192,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest { event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject(); - assertThat(responseData.has("value")).isTrue(); + assertThat(responseData.has("values")).isTrue(); assertThat(responseData.get("values").getAsJsonObject().get("attr1").getAsString()).isEqualTo(sharedAttributes.get("attr1").getAsString()); assertThat(responseData.get("values").getAsJsonObject().get("attr2").getAsString()).isEqualTo(sharedAttributes.get("attr2").getAsString()); @@ -304,11 +309,19 @@ public class MqttGatewayClientTest extends AbstractContainerTest { JsonObject serverRpcPayload = new JsonObject(); serverRpcPayload.addProperty("method", "getValue"); serverRpcPayload.addProperty("params", true); - - JsonNode response = testRestClient.postServerSideRpc(createdDevice.getId(), mapper.readTree(serverRpcPayload.toString())); + ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(getClass().getSimpleName()))); + ListenableFuture future = service.submit(() -> { + try { + return testRestClient.postServerSideRpc(createdDevice.getId(), mapper.readTree(serverRpcPayload.toString())); + } catch (IOException e) { + return null; + } + }); // Wait for RPC call from the server and send the response MqttEvent requestFromServer = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); + service.shutdownNow(); + assertThat(requestFromServer).isNotNull(); assertThat(requestFromServer.getMessage()).isNotNull(); JsonNode requestFromServerJson = JacksonUtil.toJsonNode(requestFromServer.getMessage()); @@ -326,8 +339,9 @@ public class MqttGatewayClientTest extends AbstractContainerTest { // Send a response to the server's RPC request mqttClient.publish(gatewayRpcTopic, Unpooled.wrappedBuffer(gatewayResponse.toString().getBytes())).get(); + JsonNode serverResponse = future.get(5 * timeoutMultiplier, TimeUnit.SECONDS); - assertThat(response).isEqualTo(clientResponse.getAsJsonObject()); + assertThat(serverResponse).isEqualTo(mapper.readTree(clientResponse.toString())); } @Test @@ -366,7 +380,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest { TimeUnit.SECONDS.sleep(30); } - String deviceName = "mqtt_device"; + String deviceName = "mqtt_device" + RandomStringUtils.randomAlphabetic(5); mqttClient.publish("v1/gateway/connect", Unpooled.wrappedBuffer(createGatewayConnectPayload(deviceName).toString().getBytes()), MqttQoS.AT_LEAST_ONCE).get(); if (timeoutMultiplier > 1) { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java index d7edcb6699..e8ed09df18 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java @@ -17,6 +17,7 @@ package org.thingsboard.server.msa.prototypes; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; @@ -33,7 +34,7 @@ public class DevicePrototypes { String isGateway = "{\"gateway\":true}"; JsonNode additionalInfo = JacksonUtil.valueToTree(isGateway); Device gatewayDeviceTemplate = new Device(); - gatewayDeviceTemplate.setName("mqtt_gateway " + RandomStringUtils.randomAlphabetic(5)); + gatewayDeviceTemplate.setName("mqtt_gateway_" + RandomStringUtils.randomAlphabetic(5)); gatewayDeviceTemplate.setType("gateway"); gatewayDeviceTemplate.setAdditionalInfo(additionalInfo); return gatewayDeviceTemplate; diff --git a/pom.xml b/pom.xml index 1c5dea9f5c..7d5540ef20 100755 --- a/pom.xml +++ b/pom.xml @@ -133,6 +133,10 @@ 1.3.0 1.2.7 + 7.6.1 + 3.23.1 + 5.2.0 + 1.3 1.17.3 1.12 3.0.0 @@ -1618,6 +1622,30 @@ + + org.testng + testng + ${testng.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + io.rest-assured + rest-assured + ${rest-assured.version} + test + + + org.hamcrest + hamcrest-all + ${hamcrest} + test + org.awaitility awaitility From 11ee41bf341dd5500b2b91bd2928ce34cd44893f Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 7 Nov 2022 14:42:49 +0200 Subject: [PATCH 044/226] Adding partition to edge_event table --- .../main/data/upgrade/3.4.1/schema_update.sql | 8 +- .../main/data/upgrade/3.4.2/schema_update.sql | 76 +++++++++++++++++ .../install/ThingsboardInstallService.java | 7 +- .../install/SqlDatabaseUpgradeService.java | 12 +++ .../update/DefaultDataUpdateService.java | 22 +++++ .../service/ttl/EdgeEventsCleanUpService.java | 25 +++++- .../src/main/resources/thingsboard.yml | 1 + .../BaseAuditLogControllerTest.java | 1 - .../BaseEdgeEventControllerTest.java | 69 +++++++++++++++ .../resources/application-test.properties | 5 +- .../server/dao/edge/BaseEdgeEventService.java | 12 +-- .../server/dao/edge/EdgeEventDao.java | 2 + .../dao/sql/edge/JpaBaseEdgeEventDao.java | 85 ++++++++++++------- .../resources/sql/schema-entities-idx.sql | 2 + .../main/resources/sql/schema-entities.sql | 4 +- 15 files changed, 283 insertions(+), 48 deletions(-) create mode 100644 application/src/main/data/upgrade/3.4.2/schema_update.sql diff --git a/application/src/main/data/upgrade/3.4.1/schema_update.sql b/application/src/main/data/upgrade/3.4.1/schema_update.sql index 5891246b44..66169f0120 100644 --- a/application/src/main/data/upgrade/3.4.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.1/schema_update.sql @@ -55,16 +55,16 @@ CREATE OR REPLACE PROCEDURE migrate_audit_logs(IN start_time_ms BIGINT, IN end_t LANGUAGE plpgsql AS $$ DECLARE - p RECORD; + prt RECORD; partition_end_ts BIGINT; BEGIN FOR p IN SELECT DISTINCT (created_time - created_time % partition_size_ms) AS partition_ts FROM old_audit_log WHERE created_time >= start_time_ms AND created_time < end_time_ms LOOP - partition_end_ts = p.partition_ts + partition_size_ms; - RAISE NOTICE '[audit_log] Partition to create : [%-%]', p.partition_ts, partition_end_ts; + partition_end_ts = prt.partition_ts + partition_size_ms; + RAISE NOTICE '[audit_log] Partition to create : [%-%]', prt.partition_ts, partition_end_ts; EXECUTE format('CREATE TABLE IF NOT EXISTS audit_log_%s PARTITION OF audit_log ' || - 'FOR VALUES FROM ( %s ) TO ( %s )', p.partition_ts, p.partition_ts, partition_end_ts); + 'FOR VALUES FROM ( %s ) TO ( %s )', prt.partition_ts, prt.partition_ts, partition_end_ts); END LOOP; INSERT INTO audit_log diff --git a/application/src/main/data/upgrade/3.4.2/schema_update.sql b/application/src/main/data/upgrade/3.4.2/schema_update.sql new file mode 100644 index 0000000000..10c009b740 --- /dev/null +++ b/application/src/main/data/upgrade/3.4.2/schema_update.sql @@ -0,0 +1,76 @@ +-- +-- Copyright © 2016-2022 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. +-- + +DO +$$ + DECLARE table_partition RECORD; + BEGIN + -- in case of running the upgrade script a second time: + IF NOT (SELECT exists(SELECT FROM pg_tables WHERE tablename = 'old_edge_event')) THEN + ALTER TABLE edge_event RENAME TO old_edge_event; + ALTER INDEX IF EXISTS idx_edge_event_tenant_id_and_created_time RENAME TO idx_old_edge_event_tenant_id_and_created_time; + + FOR table_partition IN SELECT tablename AS name, split_part(tablename, '_', 3) AS partition_ts + FROM pg_tables WHERE tablename LIKE 'edge_event_%' + LOOP + EXECUTE format('ALTER TABLE %s RENAME TO old_edge_event_%s', table_partition.name, table_partition.partition_ts); + END LOOP; + ELSE + RAISE NOTICE 'Table old_edge_event already exists, leaving as is'; + END IF; + END; +$$; + + +CREATE TABLE IF NOT EXISTS edge_event ( + id uuid NOT NULL, + created_time bigint NOT NULL, + edge_id uuid, + edge_event_type varchar(255), + edge_event_uid varchar(255), + entity_id uuid, + edge_event_action varchar(255), + body varchar(10000000), + tenant_id uuid, + ts bigint NOT NULL + ) PARTITION BY RANGE (created_time); +CREATE INDEX IF NOT EXISTS idx_edge_event_tenant_id_and_created_time ON edge_event(tenant_id, created_time DESC); + + +CREATE OR REPLACE PROCEDURE migrate_edge_event(IN start_time_ms BIGINT, IN end_time_ms BIGINT, IN partition_size_ms BIGINT) + LANGUAGE plpgsql AS +$$ +DECLARE + p RECORD; + partition_end_ts BIGINT; +BEGIN + FOR p IN SELECT DISTINCT (created_time - created_time % partition_size_ms) AS partition_ts FROM old_edge_event + WHERE created_time >= start_time_ms AND created_time < end_time_ms + LOOP + partition_end_ts = p.partition_ts + partition_size_ms; + RAISE NOTICE '[edge_event] Partition to create : [%-%]', p.partition_ts, partition_end_ts; + EXECUTE format('CREATE TABLE IF NOT EXISTS edge_event_%s PARTITION OF edge_event ' || + 'FOR VALUES FROM ( %s ) TO ( %s )', p.partition_ts, p.partition_ts, partition_end_ts); + END LOOP; + + INSERT INTO edge_event + SELECT id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts + FROM old_edge_event + WHERE created_time >= start_time_ms AND created_time < end_time_ms; +END; +$$; + + diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 33a6d4bdd1..5fd8c47997 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -230,9 +230,14 @@ public class ThingsboardInstallService { databaseEntitiesUpgradeService.upgradeDatabase("3.4.1"); dataUpdateService.updateData("3.4.1"); log.info("Updating system data..."); + break; + case "3.4.2": + log.info("Upgrading ThingsBoard from version 3.4.2 to 3.5.0 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.4.2"); + dataUpdateService.updateData("3.4.2"); + log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); break; - //TODO update CacheCleanupService on the next version upgrade default: diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 092726d0c8..c7ccf4cd18 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -654,6 +654,18 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.error("Failed updating schema!!!", e); } break; + case "3.4.2": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.2", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + log.info("Updating schema settings..."); + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005000;"); + log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index a8cf5374a2..68c1c50140 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -64,6 +64,7 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfi import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.audit.AuditLogDao; +import org.thingsboard.server.dao.edge.EdgeEventDao; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; @@ -142,6 +143,9 @@ public class DefaultDataUpdateService implements DataUpdateService { @Autowired private AuditLogDao auditLogDao; + @Autowired + private EdgeEventDao edgeEventDao; + @Override public void updateData(String fromVersion) throws Exception { switch (fromVersion) { @@ -189,6 +193,24 @@ public class DefaultDataUpdateService implements DataUpdateService { } else { log.info("Skipping audit logs migration"); } + boolean skipEdgeEventsMigrationTemp = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false); + if (!skipEdgeEventsMigrationTemp) { + log.info("Updating data from version 3.4.1 to 3.4.2 ..."); + log.info("Starting edge events migration. Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true"); + edgeEventDao.migrateEdgeEvents(); + } else { + log.info("Skipping edge events migration"); + } + break; + case "3.5.0": + boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false); + if (!skipEdgeEventsMigration) { + log.info("Updating data from version 3.4.2 to 3.5.0 ..."); + log.info("Starting edge events migration. Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true"); + edgeEventDao.migrateEdgeEvents(); + } else { + log.info("Skipping edge events migration"); + } break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java index c4ce4e49b2..880d3eb125 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java @@ -17,15 +17,23 @@ package org.thingsboard.server.service.ttl; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.thingsboard.server.dao.edge.EdgeEventDao; import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.util.TbCoreComponent; +import java.util.concurrent.TimeUnit; + +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_COLUMN_FAMILY_NAME; + @TbCoreComponent @Slf4j @Service +@ConditionalOnExpression("${sql.ttl.edge_events.enabled:true} && ${sql.ttl.edge_events.edge_event_ttl:0} > 0") public class EdgeEventsCleanUpService extends AbstractCleanUpService { public static final String RANDOM_DELAY_INTERVAL_MS_EXPRESSION = @@ -34,20 +42,29 @@ public class EdgeEventsCleanUpService extends AbstractCleanUpService { @Value("${sql.ttl.edge_events.edge_events_ttl}") private long ttl; - @Value("${sql.ttl.edge_events.enabled}") + @Value("${sql.edge_events.partition_size:168}") + private int partitionSizeInHours; + + @Value("${sql.ttl.edge_events.enabled:true}") private boolean ttlTaskExecutionEnabled; private final EdgeEventService edgeEventService; - public EdgeEventsCleanUpService(PartitionService partitionService, EdgeEventService edgeEventService) { + private final SqlPartitioningRepository partitioningRepository; + + public EdgeEventsCleanUpService(PartitionService partitionService, EdgeEventService edgeEventService, SqlPartitioningRepository partitioningRepository) { super(partitionService); this.edgeEventService = edgeEventService; + this.partitioningRepository = partitioningRepository; } @Scheduled(initialDelayString = RANDOM_DELAY_INTERVAL_MS_EXPRESSION, fixedDelayString = "${sql.ttl.edge_events.execution_interval_ms}") public void cleanUp() { - if (ttlTaskExecutionEnabled && isSystemTenantPartitionMine()) { - edgeEventService.cleanupEvents(ttl); + long edgeEventsExpTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(ttl); + if(isSystemTenantPartitionMine()) { + edgeEventService.cleanupEvents(edgeEventsExpTime); + } else { + partitioningRepository.cleanupPartitionsCache(EDGE_EVENT_COLUMN_FAMILY_NAME, edgeEventsExpTime, TimeUnit.HOURS.toMillis(partitionSizeInHours)); } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 66a603a1a1..2a8ea20611 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -265,6 +265,7 @@ sql: batch_size: "${SQL_EDGE_EVENTS_BATCH_SIZE:1000}" batch_max_delay: "${SQL_EDGE_EVENTS_BATCH_MAX_DELAY_MS:100}" stats_print_interval_ms: "${SQL_EDGE_EVENTS_BATCH_STATS_PRINT_MS:10000}" + partition_size: "${SQL_EDGE_EVENTS_PARTITION_SIZE_HOURS:168}" # Number of hours to partition the events. The current value corresponds to one week. audit_logs: partition_size: "${SQL_AUDIT_LOGS_PARTITION_SIZE_HOURS:168}" # Default value - 1 week # Specify whether to sort entities before batch update. Should be enabled for cluster mode to avoid deadlocks diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java index c46f7d9c46..4f8d07864b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java @@ -178,7 +178,6 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest reset(partitioningRepository); AuditLog auditLog = createAuditLog(ActionType.LOGIN, tenantAdminUserId); verify(partitioningRepository).createPartitionIfNotExists(eq("audit_log"), eq(auditLog.getCreatedTime()), eq(partitionDurationInMs)); - List partitions = partitioningRepository.fetchPartitions("audit_log"); assertThat(partitions).singleElement().satisfies(partitionStartTs -> { assertThat(partitionStartTs).isEqualTo(partitioningRepository.calculatePartitionStartTime(auditLog.getCreatedTime(), partitionDurationInMs)); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java index ef66270130..ccf218c132 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java @@ -22,6 +22,9 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Tenant; @@ -29,16 +32,27 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; 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.EdgeId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.edge.EdgeEventDao; +import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; +import org.thingsboard.server.service.ttl.EdgeEventsCleanUpService; +import java.time.LocalDate; +import java.time.ZoneOffset; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @TestPropertySource(properties = { @@ -50,6 +64,18 @@ public abstract class BaseEdgeEventControllerTest extends AbstractControllerTest private Tenant savedTenant; private User tenantAdmin; + @Autowired + private EdgeEventDao edgeEventDao; + @SpyBean + private SqlPartitioningRepository partitioningRepository; + @Autowired + private EdgeEventsCleanUpService edgeEventsCleanUpService; + + @Value("#{${sql.edge_events.partition_size} * 60 * 60 * 1000}") + private long partitionDurationInMs; + @Value("${sql.ttl.edge_events.edge_event_ttl}") + private long edgeEventTtlInSec; + @Before public void beforeTest() throws Exception { loginSysAdmin(); @@ -114,6 +140,34 @@ public abstract class BaseEdgeEventControllerTest extends AbstractControllerTest Assert.assertTrue(edgeEvents.stream().anyMatch(ee -> EdgeEventType.RELATION.equals(ee.getType()))); } + @Test + public void saveEdgeEvent_thenCreatePartitionIfNotExist() { + reset(partitioningRepository); + EdgeEvent edgeEvent = createEdgeEvent(); + verify(partitioningRepository).createPartitionIfNotExists(eq("edge_event"), eq(edgeEvent.getCreatedTime()), eq(partitionDurationInMs)); + List partitions = partitioningRepository.fetchPartitions("edge_event"); + assertThat(partitions).singleElement().satisfies(partitionStartTs -> { + assertThat(partitionStartTs).isEqualTo(partitioningRepository.calculatePartitionStartTime(edgeEvent.getCreatedTime(), partitionDurationInMs)); + }); + } + + @Test + public void cleanUpEdgeEventByTtl_dropOldPartitions() { + long oldEdgeEventTs = LocalDate.of(2020, 10, 1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(); + long partitionStartTs = partitioningRepository.calculatePartitionStartTime(oldEdgeEventTs, partitionDurationInMs); + partitioningRepository.createPartitionIfNotExists("edge_event", oldEdgeEventTs, partitionDurationInMs); + List partitions = partitioningRepository.fetchPartitions("edge_event"); + assertThat(partitions).contains(partitionStartTs); + + edgeEventsCleanUpService.cleanUp(); + partitions = partitioningRepository.fetchPartitions("edge_event"); + assertThat(partitions).doesNotContain(partitionStartTs); + assertThat(partitions).allSatisfy(partitionsStart -> { + long partitionEndTs = partitionsStart + partitionDurationInMs; + assertThat(partitionEndTs).isGreaterThan(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(edgeEventTtlInSec)); + }); + } + private List findEdgeEvents(EdgeId edgeId) throws Exception { return doGetTypedWithTimePageLink("/api/edge/" + edgeId.toString() + "/events?", new TypeReference>() { @@ -134,4 +188,19 @@ public abstract class BaseEdgeEventControllerTest extends AbstractControllerTest return asset; } + private EdgeEvent createEdgeEvent() { + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setCreatedTime(System.currentTimeMillis()); + edgeEvent.setTenantId(tenantId); + edgeEvent.setAction(EdgeEventActionType.ADDED); + edgeEvent.setEntityId(tenantAdmin.getUuidId()); + edgeEvent.setType(EdgeEventType.ALARM); + try { + edgeEventDao.saveAsync(edgeEvent).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + return edgeEvent; + } + } diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index eacd1733d3..fb2fdfe0a4 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -59,4 +59,7 @@ queue.rule-engine.queues[2].processing-strategy.max-pause-between-retries=0 usage.stats.report.enabled=false sql.audit_logs.partition_size=24 -sql.ttl.audit_logs.ttl=2592000 \ No newline at end of file +sql.ttl.audit_logs.ttl=2592000 + +sql.edge_events.partition_size=168 +sql.ttl.edge_events.edge_event_ttl=2592000 \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java b/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java index f9e94af613..09322aa555 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java @@ -17,7 +17,6 @@ package org.thingsboard.server.dao.edge; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.EdgeId; @@ -30,11 +29,14 @@ import org.thingsboard.server.dao.service.DataValidator; @Slf4j public class BaseEdgeEventService implements EdgeEventService { - @Autowired - private EdgeEventDao edgeEventDao; + private final EdgeEventDao edgeEventDao; - @Autowired - private DataValidator edgeEventValidator; + private final DataValidator edgeEventValidator; + + public BaseEdgeEventService(EdgeEventDao edgeEventDao, DataValidator edgeEventValidator) { + this.edgeEventDao = edgeEventDao; + this.edgeEventValidator = edgeEventValidator; + } @Override public ListenableFuture saveAsync(EdgeEvent edgeEvent) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java index 7a43d6c065..cb31869213 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java @@ -54,4 +54,6 @@ public interface EdgeEventDao extends Dao { */ void cleanupEvents(long ttl); + void migrateEdgeEvents(); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java index df182659bf..334d5a5671 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java @@ -17,10 +17,11 @@ package org.thingsboard.server.dao.sql.edge; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.ListenableFuture; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.EdgeEvent; @@ -31,19 +32,17 @@ import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.edge.EdgeEventDao; +import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.sql.EdgeEventEntity; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; +import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; import org.thingsboard.server.dao.util.SqlDao; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; import java.util.Comparator; import java.util.Objects; import java.util.UUID; @@ -51,19 +50,25 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; - -@Slf4j @Component @SqlDao +@RequiredArgsConstructor +@Slf4j public class JpaBaseEdgeEventDao extends JpaAbstractSearchTextDao implements EdgeEventDao { private final UUID systemTenantId = NULL_UUID; - @Autowired - ScheduledLogExecutorComponent logExecutor; + private final ScheduledLogExecutorComponent logExecutor; + + private final StatsFactory statsFactory; + + private final EdgeEventRepository edgeEventRepository; - @Autowired - private StatsFactory statsFactory; + private final EdgeEventInsertRepository edgeEventInsertRepository; + + private final SqlPartitioningRepository partitioningRepository; + + private final JdbcTemplate jdbcTemplate; @Value("${sql.edge_events.batch_size:1000}") private int batchSize; @@ -74,13 +79,14 @@ public class JpaBaseEdgeEventDao extends JpaAbstractSearchTextDao queue; + @Value("${sql.edge_events.partitions_size:168}") + private int partitionSizeInHours; - @Autowired - private EdgeEventRepository edgeEventRepository; + @Value("${sql.ttl.edge_events.edge_events_ttl:2628000}") + private long edge_events_ttl; - @Autowired - private EdgeEventInsertRepository edgeEventInsertRepository; + private static final String TABLE_NAME = ModelConstants.EDGE_EVENT_COLUMN_FAMILY_NAME; + private TbSqlBlockingQueueWrapper queue; @Override protected Class getEntityClass() { @@ -140,6 +146,7 @@ public class JpaBaseEdgeEventDao extends JpaAbstractSearchTextDao 0 ? System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(edge_events_ttl) : 1480982400000L; + + long currentTime = System.currentTimeMillis(); + var partitionStepInMs = TimeUnit.HOURS.toMillis(partitionSizeInHours); + long numberOfPartitions = (currentTime - startTime) / partitionStepInMs; + + if (numberOfPartitions > 1000) { + String error = "Please adjust your edge event partitioning configuration. Configuration with partition size " + + "of " + partitionSizeInHours + " hours and corresponding TTL will use " + numberOfPartitions + " " + + "(> 1000) partitions which is not recommended!"; + log.error(error); + throw new RuntimeException(error); + } + + while (startTime < currentTime) { + var endTime = startTime + partitionStepInMs; + log.info("Migrating edge event for time period: {} - {}", startTime, endTime); + callMigrationFunction(startTime, endTime, partitionStepInMs); + startTime = endTime; } + log.info("Event edge migration finished"); + + jdbcTemplate.execute("DROP TABLE IF EXISTS old_edge_event"); + } + + private void callMigrationFunction(long startTime, long endTime, long partitionSIzeInMs) { + jdbcTemplate.update("CALL migrate_edge_event(?, ?, ?)", startTime, endTime, partitionSIzeInMs); } + } diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 34862e5af3..e7586b17a8 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -50,6 +50,8 @@ CREATE INDEX IF NOT EXISTS idx_attribute_kv_by_key_and_last_update_ts ON attribu CREATE INDEX IF NOT EXISTS idx_audit_log_tenant_id_and_created_time ON audit_log(tenant_id, created_time DESC); +CREATE INDEX IF NOT EXISTS idx_edge_event_tenant_id_and_created_time ON edge_event(tenant_id, created_time DESC); + CREATE INDEX IF NOT EXISTS idx_rpc_tenant_id_device_id ON rpc(tenant_id, device_id); CREATE INDEX IF NOT EXISTS idx_device_external_id ON device(tenant_id, external_id); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 51df863ae5..73039274a6 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -719,7 +719,7 @@ CREATE TABLE IF NOT EXISTS edge ( ); CREATE TABLE IF NOT EXISTS edge_event ( - id uuid NOT NULL CONSTRAINT edge_event_pkey PRIMARY KEY, + id uuid NOT NULL, created_time bigint NOT NULL, edge_id uuid, edge_event_type varchar(255), @@ -729,7 +729,7 @@ CREATE TABLE IF NOT EXISTS edge_event ( body varchar(10000000), tenant_id uuid, ts bigint NOT NULL -); +) PARTITION BY RANGE(created_time); CREATE TABLE IF NOT EXISTS rpc ( id uuid NOT NULL CONSTRAINT rpc_pkey PRIMARY KEY, From 5b3eb26a3bed60bd074e4d508cca406f827cd737 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 7 Nov 2022 14:45:46 +0200 Subject: [PATCH 045/226] Remove unnecessary changes in audit-log schema_update.sql --- application/src/main/data/upgrade/3.4.1/schema_update.sql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/application/src/main/data/upgrade/3.4.1/schema_update.sql b/application/src/main/data/upgrade/3.4.1/schema_update.sql index 66169f0120..5891246b44 100644 --- a/application/src/main/data/upgrade/3.4.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.1/schema_update.sql @@ -55,16 +55,16 @@ CREATE OR REPLACE PROCEDURE migrate_audit_logs(IN start_time_ms BIGINT, IN end_t LANGUAGE plpgsql AS $$ DECLARE - prt RECORD; + p RECORD; partition_end_ts BIGINT; BEGIN FOR p IN SELECT DISTINCT (created_time - created_time % partition_size_ms) AS partition_ts FROM old_audit_log WHERE created_time >= start_time_ms AND created_time < end_time_ms LOOP - partition_end_ts = prt.partition_ts + partition_size_ms; - RAISE NOTICE '[audit_log] Partition to create : [%-%]', prt.partition_ts, partition_end_ts; + partition_end_ts = p.partition_ts + partition_size_ms; + RAISE NOTICE '[audit_log] Partition to create : [%-%]', p.partition_ts, partition_end_ts; EXECUTE format('CREATE TABLE IF NOT EXISTS audit_log_%s PARTITION OF audit_log ' || - 'FOR VALUES FROM ( %s ) TO ( %s )', prt.partition_ts, prt.partition_ts, partition_end_ts); + 'FOR VALUES FROM ( %s ) TO ( %s )', p.partition_ts, p.partition_ts, partition_end_ts); END LOOP; INSERT INTO audit_log From 290e0894ff795352b835c79fa154787eb458e708 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 7 Nov 2022 15:04:32 +0200 Subject: [PATCH 046/226] added test listener for logging --- application/src/main/resources/logback.xml | 3 +- .../server/msa/AbstractContainerTest.java | 32 ++--------- .../thingsboard/server/msa/TestListener.java | 53 +++++++++++++++++++ .../msa/prototypes/DevicePrototypes.java | 4 +- pom.xml | 2 +- 5 files changed, 60 insertions(+), 34 deletions(-) create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestListener.java diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index 90ed4785df..941bd7d278 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -25,8 +25,7 @@ - - + diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java index df1fe4a522..b85c8984bc 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java @@ -20,31 +20,21 @@ import com.google.common.collect.ImmutableMap; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import io.restassured.RestAssured; -import io.restassured.filter.log.RequestLoggingFilter; -import io.restassured.filter.log.ResponseLoggingFilter; import lombok.extern.slf4j.Slf4j; -import org.apache.http.config.Registry; -import org.apache.http.config.RegistryBuilder; -import org.apache.http.conn.socket.ConnectionSocketFactory; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.TrustStrategy; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.ssl.SSLContexts; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.testng.annotations.AfterSuite; import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Listeners; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; - -import javax.net.ssl.SSLContext; import java.net.URI; import java.util.*; + @Slf4j +@Listeners(TestListener.class) public abstract class AbstractContainerTest { protected static long timeoutMultiplier = 1; @@ -161,20 +151,4 @@ public abstract class AbstractContainerTest { } } - public static HttpComponentsClientHttpRequestFactory getRequestFactoryForSelfSignedCert() throws Exception { - SSLContextBuilder builder = SSLContexts.custom(); - builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true); - SSLContext sslContext = builder.build(); - SSLConnectionSocketFactory sslSelfSigned = new SSLConnectionSocketFactory(sslContext, (s, sslSession) -> true); - - Registry socketFactoryRegistry = RegistryBuilder - .create() - .register("https", sslSelfSigned) - .build(); - - PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(socketFactoryRegistry); - CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build(); - return new HttpComponentsClientHttpRequestFactory(httpClient); - } - } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestListener.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestListener.java new file mode 100644 index 0000000000..51bc75c86a --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestListener.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2022 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.msa; + +import lombok.extern.slf4j.Slf4j; +import org.testng.ITestContext; +import org.testng.ITestResult; +import org.testng.TestListenerAdapter; + +import static org.testng.internal.Utils.log; + +@Slf4j +public class TestListener extends TestListenerAdapter { + + @Override + public void onTestStart(ITestResult result) { + super.onTestStart(result); + log.info("===>>> Test started: " + result.getName()); + } + + /** + * Invoked when a test succeeds + */ + @Override + public void onTestSuccess(ITestResult result) { + super.onTestSuccess(result); + if (result != null) { + log.info("<<<=== Test completed successfully: " + result.getName()); + } + } + + /** + * Invoked when a test fails + */ + @Override + public void onTestFailure(ITestResult result) { + super.onTestFailure(result); + log.info("<<<=== Test failed: " + result.getName()); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java index e8ed09df18..a9fc2f55e9 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java @@ -30,11 +30,11 @@ public class DevicePrototypes { return device; } - public static Device defaultGatewayPrototype() throws JsonProcessingException { + public static Device defaultGatewayPrototype() { String isGateway = "{\"gateway\":true}"; JsonNode additionalInfo = JacksonUtil.valueToTree(isGateway); Device gatewayDeviceTemplate = new Device(); - gatewayDeviceTemplate.setName("mqtt_gateway_" + RandomStringUtils.randomAlphabetic(5)); + gatewayDeviceTemplate.setName("mqtt_gateway_" + RandomStringUtils.randomAlphanumeric(5)); gatewayDeviceTemplate.setType("gateway"); gatewayDeviceTemplate.setAdditionalInfo(additionalInfo); return gatewayDeviceTemplate; diff --git a/pom.xml b/pom.xml index 7d5540ef20..38718586f6 100755 --- a/pom.xml +++ b/pom.xml @@ -1643,7 +1643,7 @@ org.hamcrest hamcrest-all - ${hamcrest} + ${hamcrest.version} test From 610baa40bfb7f9b3ba5d040cfd9e182c40bc445e Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 7 Nov 2022 17:26:17 +0200 Subject: [PATCH 047/226] updated surefire plugin to run testNG tests --- msa/black-box-tests/README.md | 4 ++++ msa/black-box-tests/pom.xml | 13 ++++++++++--- .../msa/connectivity/MqttGatewayClientTest.java | 1 - msa/black-box-tests/src/test/resources/testNG.xml | 10 ++++++++++ 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 msa/black-box-tests/src/test/resources/testNG.xml diff --git a/msa/black-box-tests/README.md b/msa/black-box-tests/README.md index a45badf44a..a60e7405a8 100644 --- a/msa/black-box-tests/README.md +++ b/msa/black-box-tests/README.md @@ -30,5 +30,9 @@ As result, in REPOSITORY column, next images should be present: mvn clean install -DblackBoxTests.skip=false -DblackBoxTests.hybridMode=true +To run the black box tests with using local env run tests in the [msa/black-box-tests](../black-box-tests) directory with runLocal property: + + mvn clean install -DblackBoxTests.skip=false -DrunLocal=true + diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index 9cd38c0c05..49f4c959e9 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -167,11 +167,18 @@ org.apache.maven.plugins maven-surefire-plugin - - **/*TestSuite.java - + + src/test/resources/testNG.xml + ${blackBoxTests.skip} + + + org.apache.maven.surefire + surefire-testng + ${surefire.version} + + diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java index 54aec26421..ab42abc88e 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java @@ -27,7 +27,6 @@ import io.netty.buffer.Unpooled; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.junit.Assert; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; diff --git a/msa/black-box-tests/src/test/resources/testNG.xml b/msa/black-box-tests/src/test/resources/testNG.xml new file mode 100644 index 0000000000..c5ad55c9d1 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/testNG.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file From a098e33d8dfcc15196795a53b8de9e634c2b15b4 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 7 Nov 2022 17:32:04 +0200 Subject: [PATCH 048/226] Set up to 3.4.2 version instead of 3.5.0 --- .../main/data/upgrade/3.4.1/schema_update.sql | 60 +++++++++++++++ .../main/data/upgrade/3.4.2/schema_update.sql | 76 ------------------- .../install/ThingsboardInstallService.java | 6 -- .../install/SqlDatabaseUpgradeService.java | 12 --- .../sql/edge/EdgeEventInsertRepository.java | 2 + 5 files changed, 62 insertions(+), 94 deletions(-) delete mode 100644 application/src/main/data/upgrade/3.4.2/schema_update.sql diff --git a/application/src/main/data/upgrade/3.4.1/schema_update.sql b/application/src/main/data/upgrade/3.4.1/schema_update.sql index 5891246b44..dd9eadcbea 100644 --- a/application/src/main/data/upgrade/3.4.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.1/schema_update.sql @@ -73,3 +73,63 @@ BEGIN WHERE created_time >= start_time_ms AND created_time < end_time_ms; END; $$; + + +DO +$$ + DECLARE table_partition RECORD; + BEGIN + -- in case of running the upgrade script a second time: + IF NOT (SELECT exists(SELECT FROM pg_tables WHERE tablename = 'old_edge_event')) THEN + ALTER TABLE edge_event RENAME TO old_edge_event; + ALTER INDEX IF EXISTS idx_edge_event_tenant_id_and_created_time RENAME TO idx_old_edge_event_tenant_id_and_created_time; + + FOR table_partition IN SELECT tablename AS name, split_part(tablename, '_', 3) AS partition_ts + FROM pg_tables WHERE tablename LIKE 'edge_event_%' + LOOP + EXECUTE format('ALTER TABLE %s RENAME TO old_edge_event_%s', table_partition.name, table_partition.partition_ts); + END LOOP; + ELSE + RAISE NOTICE 'Table old_edge_event already exists, leaving as is'; + END IF; +END; +$$; + + +CREATE TABLE IF NOT EXISTS edge_event ( + id uuid NOT NULL, + created_time bigint NOT NULL, + edge_id uuid, + edge_event_type varchar(255), + edge_event_uid varchar(255), + entity_id uuid, + edge_event_action varchar(255), + body varchar(10000000), + tenant_id uuid, + ts bigint NOT NULL + ) PARTITION BY RANGE (created_time); +CREATE INDEX IF NOT EXISTS idx_edge_event_tenant_id_and_created_time ON edge_event(tenant_id, created_time DESC); + + +CREATE OR REPLACE PROCEDURE migrate_edge_event(IN start_time_ms BIGINT, IN end_time_ms BIGINT, IN partition_size_ms BIGINT) + LANGUAGE plpgsql AS +$$ +DECLARE + p RECORD; + partition_end_ts BIGINT; +BEGIN + FOR p IN SELECT DISTINCT (created_time - created_time % partition_size_ms) AS partition_ts FROM old_edge_event + WHERE created_time >= start_time_ms AND created_time < end_time_ms + LOOP + partition_end_ts = p.partition_ts + partition_size_ms; + RAISE NOTICE '[edge_event] Partition to create : [%-%]', p.partition_ts, partition_end_ts; + EXECUTE format('CREATE TABLE IF NOT EXISTS edge_event_%s PARTITION OF edge_event ' || + 'FOR VALUES FROM ( %s ) TO ( %s )', p.partition_ts, p.partition_ts, partition_end_ts); + END LOOP; + + INSERT INTO edge_event + SELECT id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts + FROM old_edge_event + WHERE created_time >= start_time_ms AND created_time < end_time_ms; +END; +$$; \ No newline at end of file diff --git a/application/src/main/data/upgrade/3.4.2/schema_update.sql b/application/src/main/data/upgrade/3.4.2/schema_update.sql deleted file mode 100644 index 10c009b740..0000000000 --- a/application/src/main/data/upgrade/3.4.2/schema_update.sql +++ /dev/null @@ -1,76 +0,0 @@ --- --- Copyright © 2016-2022 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. --- - -DO -$$ - DECLARE table_partition RECORD; - BEGIN - -- in case of running the upgrade script a second time: - IF NOT (SELECT exists(SELECT FROM pg_tables WHERE tablename = 'old_edge_event')) THEN - ALTER TABLE edge_event RENAME TO old_edge_event; - ALTER INDEX IF EXISTS idx_edge_event_tenant_id_and_created_time RENAME TO idx_old_edge_event_tenant_id_and_created_time; - - FOR table_partition IN SELECT tablename AS name, split_part(tablename, '_', 3) AS partition_ts - FROM pg_tables WHERE tablename LIKE 'edge_event_%' - LOOP - EXECUTE format('ALTER TABLE %s RENAME TO old_edge_event_%s', table_partition.name, table_partition.partition_ts); - END LOOP; - ELSE - RAISE NOTICE 'Table old_edge_event already exists, leaving as is'; - END IF; - END; -$$; - - -CREATE TABLE IF NOT EXISTS edge_event ( - id uuid NOT NULL, - created_time bigint NOT NULL, - edge_id uuid, - edge_event_type varchar(255), - edge_event_uid varchar(255), - entity_id uuid, - edge_event_action varchar(255), - body varchar(10000000), - tenant_id uuid, - ts bigint NOT NULL - ) PARTITION BY RANGE (created_time); -CREATE INDEX IF NOT EXISTS idx_edge_event_tenant_id_and_created_time ON edge_event(tenant_id, created_time DESC); - - -CREATE OR REPLACE PROCEDURE migrate_edge_event(IN start_time_ms BIGINT, IN end_time_ms BIGINT, IN partition_size_ms BIGINT) - LANGUAGE plpgsql AS -$$ -DECLARE - p RECORD; - partition_end_ts BIGINT; -BEGIN - FOR p IN SELECT DISTINCT (created_time - created_time % partition_size_ms) AS partition_ts FROM old_edge_event - WHERE created_time >= start_time_ms AND created_time < end_time_ms - LOOP - partition_end_ts = p.partition_ts + partition_size_ms; - RAISE NOTICE '[edge_event] Partition to create : [%-%]', p.partition_ts, partition_end_ts; - EXECUTE format('CREATE TABLE IF NOT EXISTS edge_event_%s PARTITION OF edge_event ' || - 'FOR VALUES FROM ( %s ) TO ( %s )', p.partition_ts, p.partition_ts, partition_end_ts); - END LOOP; - - INSERT INTO edge_event - SELECT id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts - FROM old_edge_event - WHERE created_time >= start_time_ms AND created_time < end_time_ms; -END; -$$; - - diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 5fd8c47997..39953ef014 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -230,12 +230,6 @@ public class ThingsboardInstallService { databaseEntitiesUpgradeService.upgradeDatabase("3.4.1"); dataUpdateService.updateData("3.4.1"); log.info("Updating system data..."); - break; - case "3.4.2": - log.info("Upgrading ThingsBoard from version 3.4.2 to 3.5.0 ..."); - databaseEntitiesUpgradeService.upgradeDatabase("3.4.2"); - dataUpdateService.updateData("3.4.2"); - log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); break; //TODO update CacheCleanupService on the next version upgrade diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index c7ccf4cd18..092726d0c8 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -654,18 +654,6 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.error("Failed updating schema!!!", e); } break; - case "3.4.2": - try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { - log.info("Updating schema ..."); - schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.2", SCHEMA_UPDATE_SQL); - loadSql(schemaUpdateFile, conn); - log.info("Updating schema settings..."); - conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005000;"); - log.info("Schema updated."); - } catch (Exception e) { - log.error("Failed updating schema!!!", e); - } - break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventInsertRepository.java index 694d68556f..ec7a6ccfab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventInsertRepository.java @@ -51,9 +51,11 @@ public class EdgeEventInsertRepository { protected void doInTransactionWithoutResult(TransactionStatus status) { jdbcTemplate.batchUpdate(INSERT, new BatchPreparedStatementSetter() { @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { EdgeEventEntity edgeEvent = entities.get(i); ps.setObject(1, edgeEvent.getId()); + ps.setLong(2, edgeEvent.getCreatedTime()); ps.setObject(3, edgeEvent.getEdgeId()); ps.setString(4, edgeEvent.getEdgeEventType().name()); From d0090f8b7a195de25416f39cef059e4d84fb334f Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 7 Nov 2022 17:52:44 +0200 Subject: [PATCH 049/226] Removed unnecessary unused case for DataUpdateService --- .../server/install/ThingsboardInstallService.java | 1 + .../install/update/DefaultDataUpdateService.java | 10 ---------- .../server/controller/BaseAuditLogControllerTest.java | 1 + .../server/dao/sql/edge/EdgeEventInsertRepository.java | 2 -- .../server/dao/sql/edge/JpaBaseEdgeEventDao.java | 1 + 5 files changed, 3 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 39953ef014..33a6d4bdd1 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -232,6 +232,7 @@ public class ThingsboardInstallService { log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); break; + //TODO update CacheCleanupService on the next version upgrade default: diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index 68c1c50140..ea6d278844 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -202,16 +202,6 @@ public class DefaultDataUpdateService implements DataUpdateService { log.info("Skipping edge events migration"); } break; - case "3.5.0": - boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false); - if (!skipEdgeEventsMigration) { - log.info("Updating data from version 3.4.2 to 3.5.0 ..."); - log.info("Starting edge events migration. Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true"); - edgeEventDao.migrateEdgeEvents(); - } else { - log.info("Skipping edge events migration"); - } - break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java index 4f8d07864b..c46f7d9c46 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java @@ -178,6 +178,7 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest reset(partitioningRepository); AuditLog auditLog = createAuditLog(ActionType.LOGIN, tenantAdminUserId); verify(partitioningRepository).createPartitionIfNotExists(eq("audit_log"), eq(auditLog.getCreatedTime()), eq(partitionDurationInMs)); + List partitions = partitioningRepository.fetchPartitions("audit_log"); assertThat(partitions).singleElement().satisfies(partitionStartTs -> { assertThat(partitionStartTs).isEqualTo(partitioningRepository.calculatePartitionStartTime(auditLog.getCreatedTime(), partitionDurationInMs)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventInsertRepository.java index ec7a6ccfab..694d68556f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventInsertRepository.java @@ -51,11 +51,9 @@ public class EdgeEventInsertRepository { protected void doInTransactionWithoutResult(TransactionStatus status) { jdbcTemplate.batchUpdate(INSERT, new BatchPreparedStatementSetter() { @Override - public void setValues(PreparedStatement ps, int i) throws SQLException { EdgeEventEntity edgeEvent = entities.get(i); ps.setObject(1, edgeEvent.getId()); - ps.setLong(2, edgeEvent.getCreatedTime()); ps.setObject(3, edgeEvent.getEdgeId()); ps.setString(4, edgeEvent.getEdgeEventType().name()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java index 334d5a5671..a0efcde00e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java @@ -86,6 +86,7 @@ public class JpaBaseEdgeEventDao extends JpaAbstractSearchTextDao queue; @Override From f9a4d87c39677ee23fe986890a17ace1731071a0 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 7 Nov 2022 18:01:46 +0200 Subject: [PATCH 050/226] refactoring --- .../server/msa/AbstractContainerTest.java | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java index b85c8984bc..97d77ad9bf 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; import org.apache.http.conn.ssl.TrustStrategy; import org.apache.http.ssl.SSLContextBuilder; @@ -37,10 +36,8 @@ import java.util.*; @Listeners(TestListener.class) public abstract class AbstractContainerTest { protected static long timeoutMultiplier = 1; - protected ObjectMapper mapper = new ObjectMapper(); - protected JsonParser jsonParser = new JsonParser(); - protected ContainerTestSuite containerTestSuite = ContainerTestSuite.getInstance(); + private static final ContainerTestSuite containerTestSuite = ContainerTestSuite.getInstance(); protected static TestRestClient testRestClient; @BeforeSuite @@ -95,27 +92,6 @@ public abstract class AbstractContainerTest { .build(); } - protected JsonObject createGatewayConnectPayload(String deviceName){ - JsonObject payload = new JsonObject(); - payload.addProperty("device", deviceName); - return payload; - } - - protected JsonObject createGatewayPayload(String deviceName, long ts){ - JsonObject payload = new JsonObject(); - payload.add(deviceName, createGatewayTelemetryArray(ts)); - return payload; - } - - protected JsonArray createGatewayTelemetryArray(long ts){ - JsonArray telemetryArray = new JsonArray(); - if (ts > 0) - telemetryArray.add(createPayload(ts)); - else - telemetryArray.add(createPayload()); - return telemetryArray; - } - protected JsonObject createPayload(long ts) { JsonObject values = createPayload(); JsonObject payload = new JsonObject(); From 0873b9e1581be217b757e23312c05fc8ceb2e933 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Mon, 7 Nov 2022 18:12:28 +0200 Subject: [PATCH 051/226] Improvements to #7434 --- .../server/controller/AuthController.java | 1 - .../server/controller/UserController.java | 1 - .../auth/DefaultTokenOutdatingService.java | 14 +++-- .../security/auth/TokenOutdatingService.java | 3 +- .../src/main/resources/thingsboard.yml | 8 +-- .../security/auth/TokenOutdatingTest.java | 60 +++++++++++-------- .../server/cache/CacheSpecsMap.java | 14 +++++ .../cache/TbCaffeineCacheConfiguration.java | 2 + .../server/common/data/CacheConstants.java | 2 +- .../server/dao/user/UserServiceImpl.java | 1 - 10 files changed, 67 insertions(+), 39 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index d098b1edec..f98de380ef 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -42,7 +42,6 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.UserCredentials; -import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; import org.thingsboard.server.common.data.security.model.SecuritySettings; diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index fe07c4b49c..cea9d2e95a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -43,7 +43,6 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; -import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.user.TbUserService; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java index fecc17d33b..74230d93ec 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.auth; import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.TbTransactionalCache; @@ -31,11 +32,16 @@ import java.util.Optional; import static java.util.concurrent.TimeUnit.MILLISECONDS; @Service -@RequiredArgsConstructor public class DefaultTokenOutdatingService implements TokenOutdatingService { + private final TbTransactionalCache cache; private final JwtTokenFactory tokenFactory; + public DefaultTokenOutdatingService(@Qualifier("UsersSessionInvalidation") TbTransactionalCache cache, JwtTokenFactory tokenFactory) { + this.cache = cache; + this.tokenFactory = tokenFactory; + } + @EventListener(classes = UserAuthDataChangedEvent.class) public void onUserAuthDataChanged(UserAuthDataChangedEvent event) { if (StringUtils.hasText(event.getId())) { @@ -48,10 +54,10 @@ public class DefaultTokenOutdatingService implements TokenOutdatingService { Claims claims = tokenFactory.parseTokenClaims(token).getBody(); long issueTime = claims.getIssuedAt().getTime(); String sessionId = claims.get("sessionId", String.class); - if (sessionId == null) { - return isTokenOutdated(issueTime, userId.toString()); + if (isTokenOutdated(issueTime, userId.toString())){ + return true; } else { - return isTokenOutdated(issueTime, userId.toString()) || isTokenOutdated(issueTime, sessionId); + return sessionId != null && isTokenOutdated(issueTime, sessionId); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java index 9013a85553..02c1bf685a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java @@ -16,11 +16,10 @@ package org.thingsboard.server.service.security.auth; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.model.JwtToken; public interface TokenOutdatingService { - void onUserAuthDataChanged(UserAuthDataChangedEvent event); boolean isOutdated(JwtToken token, UserId userId); + } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index ecb15eb03c..4fae947883 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -109,7 +109,7 @@ security: # JWT Token parameters jwt: tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}" # Number of seconds (2.5 hours) - refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week) + refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week). tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}" tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" # Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator @@ -420,9 +420,9 @@ cache: attributes: timeToLiveInMinutes: "${CACHE_SPECS_ATTRIBUTES_TTL:1440}" maxSize: "${CACHE_SPECS_ATTRIBUTES_MAX_SIZE:100000}" - usersUpdateTime: - # MUST be the same as jwt refresh token expiration time, the value here represents 604800 seconds in minutes - timeToLiveInMinutes: "${CACHE_SPECS_USERS_UPDATE_TIME_TTL:10080}" + userSessionsInvalidation: + # The value of this TTL is ignored and replaced by JWT refresh token expiration time + timeToLiveInMinutes: "0" maxSize: "${CACHE_SPECS_USERS_UPDATE_TIME_MAX_SIZE:10000}" otaPackages: timeToLiveInMinutes: "${CACHE_SPECS_OTA_PACKAGES_TTL:60}" diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java index dddd1f8a71..c84253b92c 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java @@ -21,6 +21,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootContextLoader; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.ComponentScan; import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.test.annotation.DirtiesContext; @@ -69,8 +70,9 @@ import static org.mockito.Mockito.when; "security.jwt.tokenIssuer=test.io", "security.jwt.tokenSigningKey=secret", "security.jwt.tokenExpirationTime=600", - "security.jwt.refreshTokenExpTime=60", - "cache.specs.usersUpdateTime.timeToLiveInMinutes=1" + "security.jwt.refreshTokenExpTime=15", + // explicitly set the wrong value to check that it is NOT used. + "cache.specs.userSessionsInvalidation.timeToLiveInMinutes=2" }) public class TokenOutdatingTest { private JwtAuthenticationProvider accessTokenAuthenticationProvider; @@ -79,6 +81,8 @@ public class TokenOutdatingTest { @Autowired private TokenOutdatingService tokenOutdatingService; @Autowired + private ApplicationEventPublisher eventPublisher; + @Autowired private JwtTokenFactory tokenFactory; private SecurityUser securityUser; @@ -107,8 +111,9 @@ public class TokenOutdatingTest { public void testOutdateOldUserTokens() throws Exception { JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser); - SECONDS.sleep(1); // need to wait before outdating so that outdatage time is strictly after token issue time - tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId())); + // Token outdatage time is rounded to 1 sec. Need to wait before outdating so that outdatage time is strictly after token issue time + SECONDS.sleep(1); + eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); SECONDS.sleep(1); @@ -126,7 +131,7 @@ public class TokenOutdatingTest { }); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId())); + eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); assertThrows(JwtExpiredTokenException.class, () -> { accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken)); @@ -142,28 +147,33 @@ public class TokenOutdatingTest { }); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId())); + eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); assertThrows(CredentialsExpiredException.class, () -> { refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken)); }); } - @Test - public void testTokensOutdatageTimeRemovalFromCache() throws Exception { - JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser); - - SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId())); - - SECONDS.sleep(1); - - assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); - - SECONDS.sleep(60); - - assertFalse(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); - } + // This test takes too long to run and is basically testing the cache logic +// @Test +// public void testTokensOutdatageTimeRemovalFromCache() throws Exception { +// JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser); +// +// SECONDS.sleep(1); +// eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); +// +// SECONDS.sleep(1); +// +// assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); +// +// SECONDS.sleep(30); // refreshTokenExpTime/2 +// +// assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); +// +// SECONDS.sleep(30 + 1); // refreshTokenExpTime/2 + 1 +// +// assertFalse(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); +// } @Test public void testOnlyOneTokenExpired() throws InterruptedException { @@ -178,7 +188,7 @@ public class TokenOutdatingTest { SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserSessionInvalidationEvent(securityUser.getSessionId())); + eventPublisher.publishEvent(new UserSessionInvalidationEvent(securityUser.getSessionId())); assertThrows(JwtExpiredTokenException.class, () -> { accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken))); @@ -206,14 +216,14 @@ public class TokenOutdatingTest { SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId())); + eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); assertThrows(JwtExpiredTokenException.class, () -> { - accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken))); + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken))); }); assertThrows(JwtExpiredTokenException.class, () -> { - accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(anotherJwtToken))); + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(anotherJwtToken))); }); } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java b/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java index 594bfff146..caf1c1f109 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java @@ -17,9 +17,12 @@ package org.thingsboard.server.cache; import lombok.Data; import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; +import org.thingsboard.server.common.data.CacheConstants; +import javax.annotation.PostConstruct; import java.util.Map; @Configuration @@ -27,7 +30,18 @@ import java.util.Map; @Data public class CacheSpecsMap { + @Value("${security.jwt.refreshTokenExpTime}") + private int refreshTokenExpTime; + @Getter private Map specs; + @PostConstruct + public void replaceTheJWTTokenRefreshExpTime() { + var cacheSpecs = specs.get(CacheConstants.USERS_SESSION_INVALIDATION_CACHE); + if (cacheSpecs != null) { + cacheSpecs.setTimeToLiveInMinutes((refreshTokenExpTime / 60) + 1); + } + } + } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java index 9cb139e26a..198404de2e 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java @@ -20,6 +20,7 @@ import com.github.benmanes.caffeine.cache.RemovalCause; import com.github.benmanes.caffeine.cache.Ticker; import com.github.benmanes.caffeine.cache.Weigher; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; @@ -71,6 +72,7 @@ public class TbCaffeineCacheConfiguration { } private CaffeineCache buildCache(String name, CacheSpecs cacheSpec) { + final Caffeine caffeineBuilder = Caffeine.newBuilder() .weigher(collectionSafeWeigher()) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 3199b1c0cc..c6fbe3be5f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -32,7 +32,7 @@ public class CacheConstants { public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ATTRIBUTES_CACHE = "attributes"; - public static final String USERS_SESSION_INVALIDATION_CACHE = "usersUpdateTime"; + public static final String USERS_SESSION_INVALIDATION_CACHE = "userSessionsInvalidation"; public static final String OTA_PACKAGE_CACHE = "otaPackages"; public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData"; public static final String REPOSITORY_SETTINGS_CACHE = "repositorySettings"; 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 d4f0dde0a3..41d121b889 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 @@ -37,7 +37,6 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.UserCredentials; -import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.IncorrectParameterException; From fba0cd031b8f4cbd25c5be853b0dab3b152924f0 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 7 Nov 2022 18:13:16 +0200 Subject: [PATCH 052/226] Refactoring code and optimize import --- application/src/main/data/upgrade/3.4.1/schema_update.sql | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/application/src/main/data/upgrade/3.4.1/schema_update.sql b/application/src/main/data/upgrade/3.4.1/schema_update.sql index dd9eadcbea..0f4354c0ca 100644 --- a/application/src/main/data/upgrade/3.4.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.1/schema_update.sql @@ -95,7 +95,6 @@ $$ END; $$; - CREATE TABLE IF NOT EXISTS edge_event ( id uuid NOT NULL, created_time bigint NOT NULL, @@ -110,7 +109,6 @@ CREATE TABLE IF NOT EXISTS edge_event ( ) PARTITION BY RANGE (created_time); CREATE INDEX IF NOT EXISTS idx_edge_event_tenant_id_and_created_time ON edge_event(tenant_id, created_time DESC); - CREATE OR REPLACE PROCEDURE migrate_edge_event(IN start_time_ms BIGINT, IN end_time_ms BIGINT, IN partition_size_ms BIGINT) LANGUAGE plpgsql AS $$ @@ -132,4 +130,4 @@ BEGIN FROM old_edge_event WHERE created_time >= start_time_ms AND created_time < end_time_ms; END; -$$; \ No newline at end of file +$$; From ebb29ca2007088776857c17cadf98a116938513a Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 7 Nov 2022 18:17:12 +0200 Subject: [PATCH 053/226] Refactoring code and optimize import --- .../server/service/ttl/EdgeEventsCleanUpService.java | 1 - .../src/test/resources/application-test.properties | 2 +- .../server/dao/edge/BaseEdgeEventService.java | 7 ++----- .../server/dao/sql/edge/JpaBaseEdgeEventDao.java | 11 +++++------ 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java index 880d3eb125..375c9b3932 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java @@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import org.thingsboard.server.dao.edge.EdgeEventDao; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; import org.thingsboard.server.queue.discovery.PartitionService; diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index fb2fdfe0a4..e4a36c151d 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -62,4 +62,4 @@ sql.audit_logs.partition_size=24 sql.ttl.audit_logs.ttl=2592000 sql.edge_events.partition_size=168 -sql.ttl.edge_events.edge_event_ttl=2592000 \ No newline at end of file +sql.ttl.edge_events.edge_event_ttl=2592000 diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java b/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java index 09322aa555..be78866e7a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.edge; import com.google.common.util.concurrent.ListenableFuture; +import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.edge.EdgeEvent; @@ -27,17 +28,13 @@ import org.thingsboard.server.dao.service.DataValidator; @Service @Slf4j +@AllArgsConstructor public class BaseEdgeEventService implements EdgeEventService { private final EdgeEventDao edgeEventDao; private final DataValidator edgeEventValidator; - public BaseEdgeEventService(EdgeEventDao edgeEventDao, DataValidator edgeEventValidator) { - this.edgeEventDao = edgeEventDao; - this.edgeEventValidator = edgeEventValidator; - } - @Override public ListenableFuture saveAsync(EdgeEvent edgeEvent) { edgeEventValidator.validate(edgeEvent, EdgeEvent::getTenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java index a0efcde00e..6f194d03cb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java @@ -1,12 +1,12 @@ /** * Copyright © 2016-2022 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 - * + *

+ * 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. @@ -50,6 +50,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; + @Component @SqlDao @RequiredArgsConstructor @@ -223,12 +224,10 @@ public class JpaBaseEdgeEventDao extends JpaAbstractSearchTextDao Date: Mon, 7 Nov 2022 18:24:05 +0200 Subject: [PATCH 054/226] Fix header --- .../server/dao/sql/edge/JpaBaseEdgeEventDao.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java index 6f194d03cb..ada3870f97 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java @@ -1,12 +1,12 @@ /** * Copyright © 2016-2022 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 - *

+ * + * 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. From 30edf4da3c2fd2417b5c39664b7016b5409893a7 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 7 Nov 2022 18:25:41 +0200 Subject: [PATCH 055/226] refactoring --- .../server/msa/AbstractContainerTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java index 97d77ad9bf..e9e6036771 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; import org.apache.http.conn.ssl.TrustStrategy; import org.apache.http.ssl.SSLContextBuilder; @@ -37,6 +38,7 @@ import java.util.*; public abstract class AbstractContainerTest { protected static long timeoutMultiplier = 1; protected ObjectMapper mapper = new ObjectMapper(); + protected JsonParser jsonParser = new JsonParser(); private static final ContainerTestSuite containerTestSuite = ContainerTestSuite.getInstance(); protected static TestRestClient testRestClient; @@ -92,6 +94,27 @@ public abstract class AbstractContainerTest { .build(); } + protected JsonObject createGatewayConnectPayload(String deviceName){ + JsonObject payload = new JsonObject(); + payload.addProperty("device", deviceName); + return payload; + } + + protected JsonObject createGatewayPayload(String deviceName, long ts){ + JsonObject payload = new JsonObject(); + payload.add(deviceName, createGatewayTelemetryArray(ts)); + return payload; + } + + protected JsonArray createGatewayTelemetryArray(long ts){ + JsonArray telemetryArray = new JsonArray(); + if (ts > 0) + telemetryArray.add(createPayload(ts)); + else + telemetryArray.add(createPayload()); + return telemetryArray; + } + protected JsonObject createPayload(long ts) { JsonObject values = createPayload(); JsonObject payload = new JsonObject(); From 71af93eb4a8c92ef55e041abba8aa2e6efb7f1bd Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 7 Nov 2022 18:59:27 +0200 Subject: [PATCH 056/226] fixed test --- .../thingsboard/server/msa/prototypes/DevicePrototypes.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java index a9fc2f55e9..7db35460a9 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java @@ -15,9 +15,7 @@ */ package org.thingsboard.server.msa.prototypes; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; @@ -32,7 +30,7 @@ public class DevicePrototypes { public static Device defaultGatewayPrototype() { String isGateway = "{\"gateway\":true}"; - JsonNode additionalInfo = JacksonUtil.valueToTree(isGateway); + JsonNode additionalInfo = JacksonUtil.toJsonNode(isGateway); Device gatewayDeviceTemplate = new Device(); gatewayDeviceTemplate.setName("mqtt_gateway_" + RandomStringUtils.randomAlphanumeric(5)); gatewayDeviceTemplate.setType("gateway"); From a0041ed624b1b7acda8c9ed1e612771ea5b021ac Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 8 Nov 2022 10:48:39 +0200 Subject: [PATCH 057/226] Fix tests --- .../main/java/org/thingsboard/server/cache/CacheSpecsMap.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java b/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java index caf1c1f109..808bfc82f7 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java @@ -30,7 +30,7 @@ import java.util.Map; @Data public class CacheSpecsMap { - @Value("${security.jwt.refreshTokenExpTime}") + @Value("${security.jwt.refreshTokenExpTime:604800}") private int refreshTokenExpTime; @Getter From 9def5a30c680d5ec29d319ccd5afec089f971f43 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 8 Nov 2022 10:55:28 +0200 Subject: [PATCH 058/226] Add missing method to StringUtils --- .../java/org/thingsboard/server/common/data/StringUtils.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java index 9acbb15646..fbc70aa372 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java @@ -156,6 +156,10 @@ public class StringUtils { return false; } + public static boolean contains(final CharSequence seq, final CharSequence searchSeq) { + return org.apache.commons.lang3.StringUtils.contains(seq, searchSeq); + } + public static String randomNumeric(int length) { return RandomStringUtils.randomNumeric(length); } From a02a276cf9b2836cf5893fc09348d14da8e62bbd Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Tue, 8 Nov 2022 10:59:38 +0200 Subject: [PATCH 059/226] Fix code formatting issue in TenantIdLoaderTest --- .../org/thingsboard/rule/engine/util/TenantIdLoaderTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 00e8a8c6c8..8a53148204 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -321,9 +321,9 @@ public class TenantIdLoaderTest { private void checkTenant(TenantId checkTenantId, boolean equals) { for (EntityType entityType : EntityType.values()) { EntityId entityId; - if(EntityType.TENANT.equals(entityType)){ + if (EntityType.TENANT.equals(entityType)) { entityId = tenantId; - } else if(EntityType.TENANT_PROFILE.equals(entityType)){ + } else if (EntityType.TENANT_PROFILE.equals(entityType)) { entityId = tenantProfileId; } else { entityId = getEntityId(entityType); From 7ce3af709be80b897decdd7402578a8bb19d28a2 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 8 Nov 2022 11:56:14 +0200 Subject: [PATCH 060/226] Fix broken test logic --- .../server/controller/AbstractWebTest.java | 2 +- .../server/system/BaseRestApiLimitsTest.java | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) 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 e41cbfbdf1..e2b7d700bd 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -375,7 +375,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } protected void login(String username, String password) throws Exception { - logout(); + resetTokens(); JsonNode tokenInfo = readResponse(doPost("/api/auth/login", new LoginRequest(username, password)).andExpect(status().isOk()), JsonNode.class); validateAndSetJwtToken(tokenInfo, username); } diff --git a/application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java b/application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java index 61e2036b12..2f7efae923 100644 --- a/application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java +++ b/application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java @@ -63,15 +63,15 @@ public abstract class BaseRestApiLimitsTest extends AbstractControllerTest { public void before() throws Exception { loginSysAdmin(); tenantProfile = getDefaultTenantProfile(); - logout(); + resetTokens(); } @After public void after() throws Exception { - logout(); + resetTokens(); loginSysAdmin(); saveTenantProfileWitConfiguration(tenantProfile, new DefaultTenantProfileConfiguration()); - logout(); + resetTokens(); service.shutdown(); } @@ -85,7 +85,7 @@ public abstract class BaseRestApiLimitsTest extends AbstractControllerTest { saveTenantProfileWitConfiguration(tenantProfile, configurationWithCustomerRestLimits); - logout(); + resetTokens(); loginCustomerUser(); @@ -105,7 +105,7 @@ public abstract class BaseRestApiLimitsTest extends AbstractControllerTest { saveTenantProfileWitConfiguration(tenantProfile, configurationWithTenantRestLimits); - logout(); + resetTokens(); loginCustomerUser(); @@ -125,7 +125,7 @@ public abstract class BaseRestApiLimitsTest extends AbstractControllerTest { saveTenantProfileWitConfiguration(tenantProfile, configurationWithTenantRestLimits); - logout(); + resetTokens(); loginTenantAdmin(); From 22118af4db7cc2fba246dc8f82f313983a46121a Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 8 Nov 2022 13:13:58 +0200 Subject: [PATCH 061/226] Added Edge RPC response/request support --- .../device/DeviceActorMessageProcessor.java | 5 +- .../service/edge/rpc/EdgeGrpcSession.java | 5 +- .../rpc/constructor/DeviceMsgConstructor.java | 46 +++++-- .../rpc/processor/DeviceEdgeProcessor.java | 113 ++++++++++++++++-- .../rpc/processor/EdgeRpcRequestMetadata.java | 28 +++++ .../server/edge/BaseDeviceEdgeTest.java | 3 +- .../server/common/data/DataConstants.java | 2 + .../common/data/edge/EdgeEventActionType.java | 3 +- common/edge-api/src/main/proto/edge.proto | 3 + .../rule/engine/api/RuleEngineRpcService.java | 2 - .../rule/engine/rpc/TbSendRPCReplyNode.java | 62 +++++++++- 11 files changed, 244 insertions(+), 28 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeRpcRequestMetadata.java diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 2ffe2002f9..9b95b188d2 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -823,8 +823,11 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { body.put("expirationTime", msg.getExpirationTime()); body.put("method", msg.getBody().getMethod()); body.put("params", msg.getBody().getParams()); + body.put("persisted", msg.isPersisted()); + body.put("retries", msg.getRetries()); + body.put("additionalInfo", msg.getAdditionalInfo()); - EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(tenantId, edgeId, EdgeEventType.DEVICE, EdgeEventActionType.RPC_CALL, deviceId, body); + EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(tenantId, edgeId, EdgeEventType.DEVICE, EdgeEventActionType.RPC_CALL_REQUEST, deviceId, body); return Futures.transform(systemContext.getEdgeEventService().saveAsync(edgeEvent), unused -> { systemContext.getClusterService().onEdgeEventUpdate(tenantId, edgeId); 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 2342997c65..aca2f8b562 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 @@ -463,7 +463,8 @@ public final class EdgeGrpcSession implements Closeable { case UNASSIGNED_FROM_CUSTOMER: case CREDENTIALS_REQUEST: case ENTITY_MERGE_REQUEST: - case RPC_CALL: + case RPC_CALL_REQUEST: + case RPC_CALL_RESPONSE: downlinkMsg = convertEntityEventToDownlink(edgeEvent); log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), this.sessionId, downlinkMsg); break; @@ -611,7 +612,7 @@ public final class EdgeGrpcSession implements Closeable { } if (uplinkMsg.getDeviceRpcCallMsgCount() > 0) { for (DeviceRpcCallMsg deviceRpcCallMsg : uplinkMsg.getDeviceRpcCallMsgList()) { - result.add(ctx.getDeviceProcessor().processDeviceRpcCallResponseFromEdge(edge.getTenantId(), deviceRpcCallMsg)); + result.add(ctx.getDeviceProcessor().processDeviceRpcCallFromEdge(edge.getTenantId(), edge, deviceRpcCallMsg)); } } if (uplinkMsg.getWidgetBundleTypesRequestMsgCount() > 0) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java index 511910dd8a..49438792fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java @@ -28,6 +28,7 @@ import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.RpcRequestMsg; +import org.thingsboard.server.gen.edge.v1.RpcResponseMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -96,26 +97,51 @@ public class DeviceMsgConstructor { .setIdLSB(deviceId.getId().getLeastSignificantBits()).build(); } - public DeviceRpcCallMsg constructDeviceRpcCallMsg(UUID deviceId, JsonNode body) { - int requestId = body.get("requestId").asInt(); - boolean oneway = body.get("oneway").asBoolean(); - UUID requestUUID = UUID.fromString(body.get("requestUUID").asText()); - long expirationTime = body.get("expirationTime").asLong(); + public DeviceRpcCallMsg constructDeviceRpcRequestMsg(UUID deviceId, JsonNode body) { + DeviceRpcCallMsg.Builder builder = constructDeviceRpcMsg(deviceId, body); + String method = body.get("method").asText(); String params = body.get("params").asText(); - RpcRequestMsg.Builder requestBuilder = RpcRequestMsg.newBuilder(); requestBuilder.setMethod(method); requestBuilder.setParams(params); - DeviceRpcCallMsg.Builder builder = DeviceRpcCallMsg.newBuilder() + builder.setRequestMsg(requestBuilder.build()); + + return builder.build(); + } + + public DeviceRpcCallMsg constructDeviceRpcResponseMsg(UUID deviceId, JsonNode body) { + DeviceRpcCallMsg.Builder builder = constructDeviceRpcMsg(deviceId, body); + + RpcResponseMsg.Builder responseBuilder = RpcResponseMsg.newBuilder(); + if (body.has("error")) { + responseBuilder.setError(body.get("error").asText()); + } else { + responseBuilder.setResponse(body.get("response").asText()); + } + builder.setResponseMsg(responseBuilder.build()); + + return builder.build(); + } + + private DeviceRpcCallMsg.Builder constructDeviceRpcMsg(UUID deviceId, JsonNode body) { + int requestId = body.get("requestId").asInt(); + boolean oneway = body.get("oneway").asBoolean(); + UUID requestUUID = UUID.fromString(body.get("requestUUID").asText()); + long expirationTime = body.get("expirationTime").asLong(); + boolean persisted = body.get("persisted").asBoolean(); + int retries = body.get("retries").asInt(); + String additionalInfo = body.get("additionalInfo").asText(); + return DeviceRpcCallMsg.newBuilder() .setDeviceIdMSB(deviceId.getMostSignificantBits()) .setDeviceIdLSB(deviceId.getLeastSignificantBits()) .setRequestUuidMSB(requestUUID.getMostSignificantBits()) .setRequestUuidLSB(requestUUID.getLeastSignificantBits()) - .setRequestId(requestId) .setExpirationTime(expirationTime) + .setRequestId(requestId) .setOneway(oneway) - .setRequestMsg(requestBuilder.build()); - return builder.build(); + .setPersisted(persisted) + .setRetries(retries) + .setAdditionalInfo(additionalInfo); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java index e09e036953..235fef7431 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java @@ -25,6 +25,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -52,6 +53,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; +import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; @@ -66,8 +68,14 @@ import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.rpc.FromDeviceRpcResponseActorMsg; +import javax.annotation.PostConstruct; +import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @Component @@ -75,11 +83,19 @@ import java.util.concurrent.locks.ReentrantLock; @TbCoreComponent public class DeviceEdgeProcessor extends BaseEdgeProcessor { + private final Map toServerRpcPendingMap = new ConcurrentHashMap<>(); + private ScheduledExecutorService scheduler; + @Autowired private DataDecodingEncodingService dataDecodingEncodingService; private static final ReentrantLock deviceCreationLock = new ReentrantLock(); + @PostConstruct + public void init(){ + this.scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("device-edge-processor-scheduler")); + } + public ListenableFuture processDeviceFromEdge(TenantId tenantId, Edge edge, DeviceUpdateMsg deviceUpdateMsg) { log.trace("[{}] onDeviceUpdate [{}] from edge [{}]", tenantId, deviceUpdateMsg, edge.getName()); switch (deviceUpdateMsg.getMsgType()) { @@ -325,8 +341,17 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { return metaData; } - public ListenableFuture processDeviceRpcCallResponseFromEdge(TenantId tenantId, DeviceRpcCallMsg deviceRpcCallMsg) { - log.trace("[{}] processDeviceRpcCallResponseMsg [{}]", tenantId, deviceRpcCallMsg); + public ListenableFuture processDeviceRpcCallFromEdge(TenantId tenantId, Edge edge, DeviceRpcCallMsg deviceRpcCallMsg) { + log.trace("[{}] processDeviceRpcCallFromEdge [{}]", tenantId, deviceRpcCallMsg); + if (deviceRpcCallMsg.hasResponseMsg()) { + return processDeviceRpcResponseFromEdge(tenantId, deviceRpcCallMsg); + } else if (deviceRpcCallMsg.hasRequestMsg()) { + return processDeviceRpcRequestFromEdge(tenantId, edge, deviceRpcCallMsg); + } + return Futures.immediateFuture(null); + } + + private ListenableFuture processDeviceRpcResponseFromEdge(TenantId tenantId, DeviceRpcCallMsg deviceRpcCallMsg) { SettableFuture futureToSet = SettableFuture.create(); UUID requestUuid = new UUID(deviceRpcCallMsg.getRequestUuidMSB(), deviceRpcCallMsg.getRequestUuidLSB()); DeviceId deviceId = new DeviceId(new UUID(deviceRpcCallMsg.getDeviceIdMSB(), deviceRpcCallMsg.getDeviceIdLSB())); @@ -357,6 +382,68 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { return futureToSet; } + private ListenableFuture processDeviceRpcRequestFromEdge(TenantId tenantId, Edge edge, DeviceRpcCallMsg deviceRpcCallMsg) { + DeviceId deviceId = new DeviceId(new UUID(deviceRpcCallMsg.getDeviceIdMSB(), deviceRpcCallMsg.getDeviceIdLSB())); + UUID requestUUID = new UUID(deviceRpcCallMsg.getRequestUuidMSB(), deviceRpcCallMsg.getRequestUuidLSB()); + try { + ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.createObjectNode(); + TbMsgMetaData metaData = new TbMsgMetaData(); + String requestId = Integer.toString(deviceRpcCallMsg.getRequestId()); + metaData.putValue("requestId", requestId); + metaData.putValue("requestUUID", requestUUID.toString()); + // ?? metaData.putValue("originServiceId", deviceRpcRequestMsg.get); + metaData.putValue("expirationTime", Long.toString(deviceRpcCallMsg.getExpirationTime())); + metaData.putValue("oneway", Boolean.toString(deviceRpcCallMsg.getOneway())); + metaData.putValue(DataConstants.PERSISTENT, Boolean.toString(deviceRpcCallMsg.getPersisted())); + + if (deviceRpcCallMsg.getRetries() > 0) { + metaData.putValue(DataConstants.RETRIES, Integer.toString(deviceRpcCallMsg.getRetries())); + } + + metaData.putValue(DataConstants.EDGE_ID, edge.getId().toString()); + + Device device = deviceService.findDeviceById(tenantId, deviceId); + if (device != null) { + metaData.putValue("deviceName", device.getName()); + metaData.putValue("deviceType", device.getType()); + metaData.putValue(DataConstants.DEVICE_ID, deviceId.getId().toString()); + } + + entityNode.put("method", deviceRpcCallMsg.getRequestMsg().getMethod()); + entityNode.put("params", deviceRpcCallMsg.getRequestMsg().getParams()); + + entityNode.put(DataConstants.ADDITIONAL_INFO, deviceRpcCallMsg.getAdditionalInfo()); + TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.TO_SERVER_RPC_REQUEST.name(), deviceId, null, metaData, + TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); + tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", device); + } + + @Override + public void onFailure(Throwable t) { + log.debug("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", device, t); + } + }); + toServerRpcPendingMap.put(requestId, new EdgeRpcRequestMetadata(tenantId, edge.getId(), deviceId)); + scheduler.schedule(() -> processTimeout(requestId), 60000, TimeUnit.MILLISECONDS); + } catch (JsonProcessingException | IllegalArgumentException e) { + log.warn("[{}] Failed to push device action to rule engine: {}", deviceId, DataConstants.ENTITY_CREATED, e); + } + + return Futures.immediateFuture(null); + } + + private void processTimeout(String requestId) { + EdgeRpcRequestMetadata data = toServerRpcPendingMap.remove(requestId); + if (data != null) { + // TODO: add failure body + saveEdgeEvent(data.getTenantId(), data.getEdgeId(), EdgeEventType.DEVICE, EdgeEventActionType.RPC_CALL_RESPONSE, + data.getDeviceId(), JacksonUtil.OBJECT_MAPPER.valueToTree("{}")); + } + } + public DownlinkMsg convertDeviceEventToDownlink(EdgeEvent edgeEvent) { DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; @@ -401,8 +488,10 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { .build(); } break; - case RPC_CALL: - return convertRpcCallEventToDownlink(edgeEvent); + case RPC_CALL_REQUEST: + return convertRpcCallRequestEventToDownlink(edgeEvent); + case RPC_CALL_RESPONSE: + return convertRpcCallResponseEventToDownlink(edgeEvent); case CREDENTIALS_REQUEST: return convertCredentialsRequestEventToDownlink(edgeEvent); case ENTITY_MERGE_REQUEST: @@ -411,10 +500,18 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { return downlinkMsg; } - private DownlinkMsg convertRpcCallEventToDownlink(EdgeEvent edgeEvent) { - log.trace("Executing convertRpcCallEventToDownlink, edgeEvent [{}]", edgeEvent); - DeviceRpcCallMsg deviceRpcCallMsg = - deviceMsgConstructor.constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody()); + private DownlinkMsg convertRpcCallRequestEventToDownlink(EdgeEvent edgeEvent) { + log.trace("Executing convertRpcCallRequestEventToDownlink, edgeEvent [{}]", edgeEvent); + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addDeviceRpcCallMsg(deviceMsgConstructor.constructDeviceRpcRequestMsg(edgeEvent.getEntityId(), edgeEvent.getBody())) + .build(); + } + + private DownlinkMsg convertRpcCallResponseEventToDownlink(EdgeEvent edgeEvent) { + log.trace("Executing convertRpcCallResponseEventToDownlink, edgeEvent [{}]", edgeEvent); + DeviceRpcCallMsg deviceRpcCallMsg = deviceMsgConstructor.constructDeviceRpcResponseMsg(edgeEvent.getEntityId(), edgeEvent.getBody()); + toServerRpcPendingMap.remove(Integer.toString(deviceRpcCallMsg.getRequestId())); return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceRpcCallMsg(deviceRpcCallMsg) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeRpcRequestMetadata.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeRpcRequestMetadata.java new file mode 100644 index 0000000000..a5a71d0743 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeRpcRequestMetadata.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2022 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; + +import lombok.Data; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class EdgeRpcRequestMetadata { + private final TenantId tenantId; + private final EdgeId edgeId; + private final DeviceId deviceId; +} diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java index 392a6814d4..8e8c616d7a 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java @@ -512,7 +512,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest { body.put("method", "test_method"); body.put("params", "{\"param1\":\"value1\"}"); - EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.RPC_CALL, device.getId().getId(), EdgeEventType.DEVICE, body); + EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.RPC_CALL_REQUEST, + device.getId().getId(), EdgeEventType.DEVICE, body); edgeImitator.expectMessageAmount(1); edgeEventService.saveAsync(edgeEvent).get(); clusterService.onEdgeEventUpdate(tenantId, edge.getId()); 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 85b9e681a1..69c36697d3 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 @@ -40,6 +40,8 @@ public class DataConstants { public static final String EXPIRATION_TIME = "expirationTime"; public static final String ADDITIONAL_INFO = "additionalInfo"; public static final String RETRIES = "retries"; + public static final String EDGE_ID = "edgeId"; + public static final String DEVICE_ID = "deviceId"; public static final String COAP_TRANSPORT_NAME = "COAP"; public static final String LWM2M_TRANSPORT_NAME = "LWM2M"; public static final String MQTT_TRANSPORT_NAME = "MQTT"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java index bba8767fca..7fae6c56b1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java @@ -28,7 +28,8 @@ public enum EdgeEventActionType { UNASSIGNED_FROM_CUSTOMER, RELATION_ADD_OR_UPDATE, RELATION_DELETED, - RPC_CALL, + RPC_CALL_REQUEST, + RPC_CALL_RESPONSE, ALARM_ACK, ALARM_CLEAR, ASSIGNED_TO_EDGE, diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index a2e3b5989b..a5785d6870 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -430,6 +430,9 @@ message DeviceRpcCallMsg { bool oneway = 7; RpcRequestMsg requestMsg = 8; RpcResponseMsg responseMsg = 9; + bool persisted = 10; + int32 retries = 11; + string additionalInfo = 12; } message RpcRequestMsg { diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java index abb5ace0f8..85596d7e7c 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java @@ -15,8 +15,6 @@ */ package org.thingsboard.rule.engine.api; -import org.thingsboard.server.common.data.id.DeviceId; - import java.util.UUID; import java.util.function.Consumer; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java index ce9b5050f0..22d13f196b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java @@ -15,15 +15,27 @@ */ package org.thingsboard.rule.engine.rpc; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.StringUtils; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; +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.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -65,9 +77,53 @@ public class TbSendRPCReplyNode implements TbNode { } else if (StringUtils.isEmpty(msg.getData())) { ctx.tellFailure(msg, new RuntimeException("Request body is empty!")); } else { - ctx.getRpcService().sendRpcReplyToDevice(serviceIdStr, UUID.fromString(sessionIdStr), Integer.parseInt(requestIdStr), msg.getData()); - ctx.tellSuccess(msg); + if (StringUtils.isNotBlank(msg.getMetaData().getValue(DataConstants.EDGE_ID))) { + saveRpcResponseToEdgeQueue(ctx, msg); + } else { + ctx.getRpcService().sendRpcReplyToDevice(serviceIdStr, UUID.fromString(sessionIdStr), Integer.parseInt(requestIdStr), msg.getData()); + ctx.tellSuccess(msg); + } } } + private void saveRpcResponseToEdgeQueue(TbContext ctx, TbMsg msg) { +// EdgeEvent edgeEvent = new EdgeEvent(); +// edgeEvent.setTenantId(tenantId); +// edgeEvent.setAction(eventAction); +// edgeEvent.setEntityId(entityId); +// edgeEvent.setType(eventType); +// edgeEvent.setBody(entityBody); +// edgeEvent.setEdgeId(edgeId); +// +// ObjectNode body = mapper.createObjectNode(); +// body.put("requestId", requestId); +// body.put("requestUUID", msg.getId().toString()); +// body.put("oneway", msg.isOneway()); +// body.put("expirationTime", msg.getExpirationTime()); +// body.put("method", msg.getBody().getMethod()); +// body.put("params", msg.getBody().getParams()); +// body.put("persisted", msg.isPersisted()); +// body.put("retries", msg.getRetries()); +// body.put("additionalInfo", msg.getAdditionalInfo()); + + EdgeId edgeId = new EdgeId(UUID.fromString(msg.getMetaData().getValue(DataConstants.EDGE_ID))); + DeviceId deviceId = new DeviceId(UUID.fromString(msg.getMetaData().getValue(DataConstants.DEVICE_ID))); + // TODO: add body + EdgeEvent edgeEvent = + EdgeUtils.constructEdgeEvent(ctx.getTenantId(), edgeId, EdgeEventType.DEVICE, + EdgeEventActionType.RPC_CALL_RESPONSE, deviceId, JacksonUtil.OBJECT_MAPPER.valueToTree("{}")); + + ListenableFuture future = ctx.getEdgeEventService().saveAsync(edgeEvent); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable Void result) { + ctx.onEdgeEventUpdate(ctx.getTenantId(), edgeId); + ctx.tellSuccess(msg); + } + + @Override + public void onFailure(Throwable t) { + } + }, ctx.getDbCallbackExecutor()); + } } From 2d2d78ae6bc656d9ed36375743cfcf5455e9e7b7 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 8 Nov 2022 15:08:22 +0100 Subject: [PATCH 062/226] fetch attribute types for upgrade improvements --- .../install/SqlDatabaseUpgradeService.java | 39 +++++++++++-------- .../server/dao/asset/AssetDao.java | 3 ++ .../server/dao/model/sql/TbPair.java | 26 +++++++++++++ .../server/dao/sql/asset/AssetRepository.java | 18 +++++---- .../server/dao/sql/asset/JpaAssetDao.java | 9 +++++ 5 files changed, 72 insertions(+), 23 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/TbPair.java diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 4b6f1e1a52..3067e545b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -34,12 +34,13 @@ import org.thingsboard.server.common.data.queue.ProcessingStrategyType; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; +import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.model.sql.TbPair; import org.thingsboard.server.dao.queue.QueueService; -import org.thingsboard.server.dao.sql.asset.AssetRepository; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration; @@ -57,7 +58,10 @@ import java.sql.SQLSyntaxErrorException; import java.sql.SQLWarning; import java.sql.Statement; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO; @@ -111,7 +115,7 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService private DeviceService deviceService; @Autowired - private AssetRepository assetRepository; + private AssetDao assetDao; @Autowired private DeviceProfileService deviceProfileService; @@ -619,24 +623,27 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService loadSql(schemaUpdateFile, conn); log.info("Creating default asset profiles..."); - PageLink pageLink = new PageLink(100); - PageData pageData; + PageLink pageLink = new PageLink(1000); + PageData> pageData; List> futures = new ArrayList<>(); + Set tenants = new HashSet<>(); do { - pageData = tenantService.findTenants(pageLink); - for (Tenant tenant : pageData.getData()) { - List assetTypes = assetRepository.findTenantAssetTypes(tenant.getUuidId()); - assetTypes.remove("default"); - - try { - futures.add(dbUpgradeExecutor.submit(() -> assetProfileService.createDefaultAssetProfile(tenant.getId()))); - } catch (Exception e) { + pageData = assetDao.getAllAssetTypes(pageLink); + for (TbPair pair : pageData.getData()) { + TenantId tenantId = new TenantId(pair.getFirst()); + String assetType = pair.getSecond(); + if (tenants.add(tenantId)) { + try { + futures.add(dbUpgradeExecutor.submit(() -> + assetProfileService.createDefaultAssetProfile(tenantId))); + } catch (Exception e) {} } - for (String assetType : assetTypes) { + + if (!"default".equals(assetType)) { try { - futures.add(dbUpgradeExecutor.submit(() -> assetProfileService.findOrCreateAssetProfile(tenant.getId(), assetType))); - } catch (Exception e) { - } + futures.add(dbUpgradeExecutor.submit(() -> + assetProfileService.findOrCreateAssetProfile(tenantId, assetType))); + } catch (Exception e) {} } } pageLink = pageLink.nextPageLink(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java index 4d67a794b6..d17181cffa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.TenantEntityDao; +import org.thingsboard.server.dao.model.sql.TbPair; import java.util.List; import java.util.Optional; @@ -222,4 +223,6 @@ public interface AssetDao extends Dao, TenantEntityDao, ExportableEntityD * @return the list of asset objects */ PageData findAssetsByTenantIdAndEdgeIdAndType(UUID tenantId, UUID edgeId, String type, PageLink pageLink); + + PageData> getAllAssetTypes(PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbPair.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbPair.java new file mode 100644 index 0000000000..4cea485575 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbPair.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2022 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.dao.model.sql; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class TbPair { + private S first; + private T second; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index eadf0d2b84..cbb68b6ccc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -23,6 +23,7 @@ import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.AssetEntity; import org.thingsboard.server.dao.model.sql.AssetInfoEntity; +import org.thingsboard.server.dao.model.sql.TbPair; import java.util.List; import java.util.UUID; @@ -70,9 +71,9 @@ public interface AssetRepository extends JpaRepository, Expor "AND a.assetProfileId = :profileId " + "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantIdAndProfileId(@Param("tenantId") UUID tenantId, - @Param("profileId") UUID profileId, - @Param("searchText") String searchText, - Pageable pageable); + @Param("profileId") UUID profileId, + @Param("searchText") String searchText, + Pageable pageable); @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo, p.name) " + "FROM AssetEntity a " + @@ -186,14 +187,17 @@ public interface AssetRepository extends JpaRepository, Expor "AND a.type = :type " + "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, - @Param("edgeId") UUID edgeId, - @Param("type") String type, - @Param("searchText") String searchText, - Pageable pageable); + @Param("edgeId") UUID edgeId, + @Param("type") String type, + @Param("searchText") String searchText, + Pageable pageable); Long countByTenantIdAndTypeIsNot(UUID tenantId, String type); @Query("SELECT externalId FROM AssetEntity WHERE id = :id") UUID getExternalIdById(@Param("id") UUID id); + @Query(value = "SELECT DISTINCT new org.thingsboard.server.dao.model.sql.TbPair(a.tenantId , a.type) FROM AssetEntity a") + Page> getAllAssetTypes(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index 489e15502e..a0c70428a5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -28,14 +28,17 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.model.sql.AssetEntity; import org.thingsboard.server.dao.model.sql.AssetInfoEntity; +import org.thingsboard.server.dao.model.sql.TbPair; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -243,6 +246,12 @@ public class JpaAssetDao extends JpaAbstractSearchTextDao im DaoUtil.toPageable(pageLink))); } + public PageData> getAllAssetTypes(PageLink pageLink) { + log.debug("Try to find all asset types and pageLink [{}]", pageLink); + return DaoUtil.pageToPageData(assetRepository.getAllAssetTypes( + DaoUtil.toPageable(pageLink, Arrays.asList(new SortOrder("tenantId"), new SortOrder("type"))))); + } + @Override public Long countByTenantId(TenantId tenantId) { return assetRepository.countByTenantIdAndTypeIsNot(tenantId.getId(), TB_SERVICE_QUEUE); From 60088164a6eda5fcd98f81c51ad40845a50068f8 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 8 Nov 2022 16:44:27 +0200 Subject: [PATCH 063/226] Fix NPE in CacheSpecMap when specs is not specified. --- .../java/org/thingsboard/server/cache/CacheSpecsMap.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java b/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java index 808bfc82f7..6910325add 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java @@ -38,9 +38,11 @@ public class CacheSpecsMap { @PostConstruct public void replaceTheJWTTokenRefreshExpTime() { - var cacheSpecs = specs.get(CacheConstants.USERS_SESSION_INVALIDATION_CACHE); - if (cacheSpecs != null) { - cacheSpecs.setTimeToLiveInMinutes((refreshTokenExpTime / 60) + 1); + if (specs != null) { + var cacheSpecs = specs.get(CacheConstants.USERS_SESSION_INVALIDATION_CACHE); + if (cacheSpecs != null) { + cacheSpecs.setTimeToLiveInMinutes((refreshTokenExpTime / 60) + 1); + } } } From 9d19879bd7c0441cea5570e09e3d153a111a4c83 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 8 Nov 2022 16:45:35 +0200 Subject: [PATCH 064/226] Improve db calls stats printing --- .../server/dao/aspect/SqlDaoCallsAspect.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/aspect/SqlDaoCallsAspect.java b/dao/src/main/java/org/thingsboard/server/dao/aspect/SqlDaoCallsAspect.java index 9c4ca8c536..e6e08dcd13 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/aspect/SqlDaoCallsAspect.java +++ b/dao/src/main/java/org/thingsboard/server/dao/aspect/SqlDaoCallsAspect.java @@ -72,7 +72,7 @@ public class SqlDaoCallsAspect { try { if (log.isTraceEnabled()) { logTopNTenants(snapshots, Comparator.comparing(DbCallStatsSnapshot::getTotalTiming).reversed(), 0, snapshot -> { - logSnapshot(snapshot, 0, Comparator.comparing(MethodCallStatsSnapshot::getTiming).reversed(), log::trace); + logSnapshot(snapshot, 0, Comparator.comparing(MethodCallStatsSnapshot::getTiming).reversed(), "timing", log::trace); }); Map> byMethodStats = new HashMap<>(); @@ -97,29 +97,33 @@ public class SqlDaoCallsAspect { } else if (log.isDebugEnabled()) { log.debug("Total calls statistics below:"); logTopNTenants(snapshots, Comparator.comparingInt(DbCallStatsSnapshot::getTotalCalls).reversed(), 10, - s -> logSnapshot(s, 10, Comparator.comparing(MethodCallStatsSnapshot::getExecutions).reversed(), log::debug)); + s -> logSnapshot(s, 10, Comparator.comparing(MethodCallStatsSnapshot::getExecutions).reversed(), "executions", log::debug)); log.debug("Total timing statistics below:"); - logTopNTenants(snapshots, Comparator.comparingLong(DbCallStatsSnapshot::getTotalTiming).reversed(), - 10, s -> logSnapshot(s, 10, Comparator.comparing(MethodCallStatsSnapshot::getTiming).reversed(), log::debug)); + logTopNTenants(snapshots, Comparator.comparingLong(DbCallStatsSnapshot::getTotalTiming).reversed(), 10, + s -> logSnapshot(s, 10, Comparator.comparing(MethodCallStatsSnapshot::getTiming).reversed(), "timing", log::debug)); log.debug("Total errors statistics below:"); - logTopNTenants(snapshots, Comparator.comparingInt(DbCallStatsSnapshot::getTotalFailure).reversed(), - 10, s -> logSnapshot(s, 10, Comparator.comparing(MethodCallStatsSnapshot::getFailures).reversed(), log::debug)); + logTopNTenants(snapshots, Comparator.comparingInt(DbCallStatsSnapshot::getTotalFailure).reversed(), 10, + s -> logSnapshot(s, 10, Comparator.comparing(MethodCallStatsSnapshot::getFailures).reversed(), "failures", log::debug)); } else if (log.isInfoEnabled()) { log.info("Total calls statistics below:"); - logTopNTenants(snapshots, Comparator.comparingInt(DbCallStatsSnapshot::getTotalFailure).reversed(), - 3, s -> logSnapshot(s, 3, Comparator.comparing(MethodCallStatsSnapshot::getFailures).reversed(), log::info)); + logTopNTenants(snapshots, Comparator.comparingInt(DbCallStatsSnapshot::getTotalCalls).reversed(), 3, + s -> logSnapshot(s, 3, Comparator.comparing(MethodCallStatsSnapshot::getExecutions).reversed(), "executions", log::info)); + log.info("Total timing statistics below:"); + logTopNTenants(snapshots, Comparator.comparingLong(DbCallStatsSnapshot::getTotalTiming).reversed(), 3, + s -> logSnapshot(s, 3, Comparator.comparing(MethodCallStatsSnapshot::getTiming).reversed(), "timing", log::info)); } } finally { statsMap.clear(); } } - private void logSnapshot(DbCallStatsSnapshot snapshot, int limit, Comparator methodStatsComparator, Consumer logger) { + private void logSnapshot(DbCallStatsSnapshot snapshot, int limit, Comparator methodStatsComparator, String sortingKey, Consumer logger) { logger.accept(String.format("[%s]: calls: %s, failures: %s, exec time: %s ", snapshot.getTenantId(), snapshot.getTotalCalls(), snapshot.getTotalFailure(), snapshot.getTotalTiming())); var stream = snapshot.getMethodStats().entrySet().stream() .sorted(Map.Entry.comparingByValue(methodStatsComparator)); if (limit > 0) { + logger.accept(String.format("[%s] Top %s methods by %s:", snapshot.getTenantId(), limit, sortingKey)); stream = stream.limit(limit); } stream.forEach(e -> { From dc1b21c5093e05c1a803000d0da2f0748608b865 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 8 Nov 2022 17:43:58 +0200 Subject: [PATCH 065/226] Print top db calls by timings on info log level --- .../org/thingsboard/server/dao/aspect/SqlDaoCallsAspect.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/aspect/SqlDaoCallsAspect.java b/dao/src/main/java/org/thingsboard/server/dao/aspect/SqlDaoCallsAspect.java index e6e08dcd13..c1d0cc60a7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/aspect/SqlDaoCallsAspect.java +++ b/dao/src/main/java/org/thingsboard/server/dao/aspect/SqlDaoCallsAspect.java @@ -105,9 +105,6 @@ public class SqlDaoCallsAspect { logTopNTenants(snapshots, Comparator.comparingInt(DbCallStatsSnapshot::getTotalFailure).reversed(), 10, s -> logSnapshot(s, 10, Comparator.comparing(MethodCallStatsSnapshot::getFailures).reversed(), "failures", log::debug)); } else if (log.isInfoEnabled()) { - log.info("Total calls statistics below:"); - logTopNTenants(snapshots, Comparator.comparingInt(DbCallStatsSnapshot::getTotalCalls).reversed(), 3, - s -> logSnapshot(s, 3, Comparator.comparing(MethodCallStatsSnapshot::getExecutions).reversed(), "executions", log::info)); log.info("Total timing statistics below:"); logTopNTenants(snapshots, Comparator.comparingLong(DbCallStatsSnapshot::getTotalTiming).reversed(), 3, s -> logSnapshot(s, 3, Comparator.comparing(MethodCallStatsSnapshot::getTiming).reversed(), "timing", log::info)); From ff3f0234a94f979053ade94425e5fb733ba5ee46 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 8 Nov 2022 17:46:11 +0200 Subject: [PATCH 066/226] Refactoring the code according to the comments after the review --- .../install/update/DefaultDataUpdateService.java | 15 ++++++--------- .../service/ttl/EdgeEventsCleanUpService.java | 2 +- .../server/dao/sql/edge/JpaBaseEdgeEventDao.java | 2 +- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index ea6d278844..be8f1db3df 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -49,11 +49,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.FilterPredicateValue; -import org.thingsboard.server.common.data.queue.ProcessingStrategy; -import org.thingsboard.server.common.data.queue.ProcessingStrategyType; -import org.thingsboard.server.common.data.queue.Queue; -import org.thingsboard.server.common.data.queue.SubmitStrategy; -import org.thingsboard.server.common.data.queue.SubmitStrategyType; +import org.thingsboard.server.common.data.queue.*; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; @@ -186,16 +182,17 @@ public class DefaultDataUpdateService implements DataUpdateService { break; case "3.4.1": boolean skipAuditLogsMigration = getEnv("TB_SKIP_AUDIT_LOGS_MIGRATION", false); - if (!skipAuditLogsMigration) { + boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false); + if (!skipAuditLogsMigration || !skipEdgeEventsMigration) { log.info("Updating data from version 3.4.1 to 3.4.2 ..."); + } + if (!skipAuditLogsMigration) { log.info("Starting audit logs migration. Can be skipped with TB_SKIP_AUDIT_LOGS_MIGRATION env variable set to true"); auditLogDao.migrateAuditLogs(); } else { log.info("Skipping audit logs migration"); } - boolean skipEdgeEventsMigrationTemp = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false); - if (!skipEdgeEventsMigrationTemp) { - log.info("Updating data from version 3.4.1 to 3.4.2 ..."); + if (!skipEdgeEventsMigration) { log.info("Starting edge events migration. Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true"); edgeEventDao.migrateEdgeEvents(); } else { diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java index 375c9b3932..a56b1963d6 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java @@ -60,7 +60,7 @@ public class EdgeEventsCleanUpService extends AbstractCleanUpService { @Scheduled(initialDelayString = RANDOM_DELAY_INTERVAL_MS_EXPRESSION, fixedDelayString = "${sql.ttl.edge_events.execution_interval_ms}") public void cleanUp() { long edgeEventsExpTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(ttl); - if(isSystemTenantPartitionMine()) { + if (ttlTaskExecutionEnabled && isSystemTenantPartitionMine()) { edgeEventService.cleanupEvents(edgeEventsExpTime); } else { partitioningRepository.cleanupPartitionsCache(EDGE_EVENT_COLUMN_FAMILY_NAME, edgeEventsExpTime, TimeUnit.HOURS.toMillis(partitionSizeInHours)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java index ada3870f97..3ab21ae6ff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java @@ -203,7 +203,7 @@ public class JpaBaseEdgeEventDao extends JpaAbstractSearchTextDao 0 ? System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(edge_events_ttl) : 1480982400000L; + long startTime = edge_events_ttl > 0 ? System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(edge_events_ttl) : 1629158400000L; long currentTime = System.currentTimeMillis(); var partitionStepInMs = TimeUnit.HOURS.toMillis(partitionSizeInHours); From 0849aa722f8f3b310d57a1be3db233f8dee479fc Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 8 Nov 2022 17:34:44 +0100 Subject: [PATCH 067/226] delete assets with a non-existent tenant and set default asset profile to the assets with xss --- .../server/service/install/SqlDatabaseUpgradeService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 3067e545b1..777284521e 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -622,6 +622,8 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.1", "schema_update_before.sql"); loadSql(schemaUpdateFile, conn); + conn.createStatement().execute("DELETE FROM asset a WHERE NOT exists(SELECT id FROM tenant WHERE id = a.tenant_id);"); + log.info("Creating default asset profiles..."); PageLink pageLink = new PageLink(1000); PageData> pageData; @@ -654,6 +656,9 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.info("Updating asset profiles..."); conn.createStatement().execute("call update_asset_profiles()"); + conn.createStatement().execute("UPDATE asset a SET asset_profile_id = " + + "(SELECT id FROM asset_profile ap WHERE ap.tenant_id = a.tenant_id AND name='default') WHERE a.asset_profile_id IS NULL;"); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.1", "schema_update_after.sql"); loadSql(schemaUpdateFile, conn); From 71d26ba18b83edd9b17a5f5b9cf0a8a9df78886d Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 9 Nov 2022 01:18:01 +0100 Subject: [PATCH 068/226] asset profile upgrade improvements --- .../install/SqlDatabaseUpgradeService.java | 36 +++++++++++++------ .../dao/sql/tenant/TenantRepository.java | 4 +++ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 777284521e..bcd3686183 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -41,6 +41,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.model.sql.TbPair; import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.dao.sql.tenant.TenantRepository; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration; @@ -111,6 +112,9 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService @Autowired private TenantService tenantService; + @Autowired + private TenantRepository tenantRepository; + @Autowired private DeviceService deviceService; @@ -628,29 +632,41 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService PageLink pageLink = new PageLink(1000); PageData> pageData; List> futures = new ArrayList<>(); - Set tenants = new HashSet<>(); + Set tenants = new HashSet<>(); do { pageData = assetDao.getAllAssetTypes(pageLink); for (TbPair pair : pageData.getData()) { TenantId tenantId = new TenantId(pair.getFirst()); String assetType = pair.getSecond(); - if (tenants.add(tenantId)) { - try { - futures.add(dbUpgradeExecutor.submit(() -> - assetProfileService.createDefaultAssetProfile(tenantId))); - } catch (Exception e) {} + if (tenants.add(pair.getFirst())) { + futures.add(dbUpgradeExecutor.submit(() -> { + try { + assetProfileService.createDefaultAssetProfile(tenantId); + } catch (Exception e) {} + })); } if (!"default".equals(assetType)) { - try { - futures.add(dbUpgradeExecutor.submit(() -> - assetProfileService.findOrCreateAssetProfile(tenantId, assetType))); - } catch (Exception e) {} + futures.add(dbUpgradeExecutor.submit(() -> { + try { + assetProfileService.findOrCreateAssetProfile(tenantId, assetType); + } catch (Exception e) {} + })); } } pageLink = pageLink.nextPageLink(); } while (pageData.hasNext()); + List tenantsWithoutProfiles = tenantRepository.getIdsNotIn(tenants); + + tenantsWithoutProfiles.forEach(uuid -> + futures.add(dbUpgradeExecutor.submit(() -> { + try { + assetProfileService.createDefaultAssetProfile(TenantId.fromUUID(uuid)); + } catch (Exception e) {} + })) + ); + Futures.allAsList(futures).get(); log.info("Updating asset profiles..."); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java index 7042dcbb16..fb563895d5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java @@ -24,6 +24,7 @@ import org.thingsboard.server.dao.model.sql.TenantEntity; import org.thingsboard.server.dao.model.sql.TenantInfoEntity; import java.util.List; +import java.util.Set; import java.util.UUID; /** @@ -54,4 +55,7 @@ public interface TenantRepository extends JpaRepository { @Query("SELECT t.id FROM TenantEntity t where t.tenantProfileId = :tenantProfileId") List findTenantIdsByTenantProfileId(@Param("tenantProfileId") UUID tenantProfileId); + @Query("SELECT t.id FROM TenantEntity t WHERE t.id NOT IN :ids") + List getIdsNotIn(@Param("ids")Set ids); + } From 545790fc5b8430357f1f5a47291a92eda36b5f5d Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 9 Nov 2022 10:18:20 +0200 Subject: [PATCH 069/226] Do not register timeout for edge processor on cloud --- .../rpc/processor/DeviceEdgeProcessor.java | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java index 235fef7431..b340b252ef 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java @@ -25,7 +25,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -68,14 +67,8 @@ import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.rpc.FromDeviceRpcResponseActorMsg; -import javax.annotation.PostConstruct; -import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @Component @@ -83,19 +76,11 @@ import java.util.concurrent.locks.ReentrantLock; @TbCoreComponent public class DeviceEdgeProcessor extends BaseEdgeProcessor { - private final Map toServerRpcPendingMap = new ConcurrentHashMap<>(); - private ScheduledExecutorService scheduler; - @Autowired private DataDecodingEncodingService dataDecodingEncodingService; private static final ReentrantLock deviceCreationLock = new ReentrantLock(); - @PostConstruct - public void init(){ - this.scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("device-edge-processor-scheduler")); - } - public ListenableFuture processDeviceFromEdge(TenantId tenantId, Edge edge, DeviceUpdateMsg deviceUpdateMsg) { log.trace("[{}] onDeviceUpdate [{}] from edge [{}]", tenantId, deviceUpdateMsg, edge.getName()); switch (deviceUpdateMsg.getMsgType()) { @@ -426,8 +411,6 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { log.debug("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", device, t); } }); - toServerRpcPendingMap.put(requestId, new EdgeRpcRequestMetadata(tenantId, edge.getId(), deviceId)); - scheduler.schedule(() -> processTimeout(requestId), 60000, TimeUnit.MILLISECONDS); } catch (JsonProcessingException | IllegalArgumentException e) { log.warn("[{}] Failed to push device action to rule engine: {}", deviceId, DataConstants.ENTITY_CREATED, e); } @@ -435,15 +418,6 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { return Futures.immediateFuture(null); } - private void processTimeout(String requestId) { - EdgeRpcRequestMetadata data = toServerRpcPendingMap.remove(requestId); - if (data != null) { - // TODO: add failure body - saveEdgeEvent(data.getTenantId(), data.getEdgeId(), EdgeEventType.DEVICE, EdgeEventActionType.RPC_CALL_RESPONSE, - data.getDeviceId(), JacksonUtil.OBJECT_MAPPER.valueToTree("{}")); - } - } - public DownlinkMsg convertDeviceEventToDownlink(EdgeEvent edgeEvent) { DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; @@ -511,7 +485,6 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { private DownlinkMsg convertRpcCallResponseEventToDownlink(EdgeEvent edgeEvent) { log.trace("Executing convertRpcCallResponseEventToDownlink, edgeEvent [{}]", edgeEvent); DeviceRpcCallMsg deviceRpcCallMsg = deviceMsgConstructor.constructDeviceRpcResponseMsg(edgeEvent.getEntityId(), edgeEvent.getBody()); - toServerRpcPendingMap.remove(Integer.toString(deviceRpcCallMsg.getRequestId())); return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceRpcCallMsg(deviceRpcCallMsg) From d84fd92f571b93b3d789421e7a0fb3aa2aec75b4 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 9 Nov 2022 09:58:15 +0100 Subject: [PATCH 070/226] refactored --- .../install/SqlDatabaseUpgradeService.java | 43 +++++++++---------- .../dao/sql/tenant/TenantRepository.java | 5 --- 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index bcd3686183..d96475ad31 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -629,23 +629,30 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService conn.createStatement().execute("DELETE FROM asset a WHERE NOT exists(SELECT id FROM tenant WHERE id = a.tenant_id);"); log.info("Creating default asset profiles..."); - PageLink pageLink = new PageLink(1000); - PageData> pageData; List> futures = new ArrayList<>(); + + PageLink pageLink = new PageLink(1000); + PageData tenantIds; + do { + tenantIds = tenantService.findTenantsIds(pageLink); + for (TenantId tenantId : tenantIds.getData()) { + futures.add(dbUpgradeExecutor.submit(() -> { + try { + assetProfileService.createDefaultAssetProfile(tenantId); + } catch (Exception e) {} + })); + } + pageLink = pageLink.nextPageLink(); + } while (tenantIds.hasNext()); + + pageLink = new PageLink(1000); + PageData> pairs; Set tenants = new HashSet<>(); do { - pageData = assetDao.getAllAssetTypes(pageLink); - for (TbPair pair : pageData.getData()) { + pairs = assetDao.getAllAssetTypes(pageLink); + for (TbPair pair : pairs.getData()) { TenantId tenantId = new TenantId(pair.getFirst()); String assetType = pair.getSecond(); - if (tenants.add(pair.getFirst())) { - futures.add(dbUpgradeExecutor.submit(() -> { - try { - assetProfileService.createDefaultAssetProfile(tenantId); - } catch (Exception e) {} - })); - } - if (!"default".equals(assetType)) { futures.add(dbUpgradeExecutor.submit(() -> { try { @@ -655,17 +662,7 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService } } pageLink = pageLink.nextPageLink(); - } while (pageData.hasNext()); - - List tenantsWithoutProfiles = tenantRepository.getIdsNotIn(tenants); - - tenantsWithoutProfiles.forEach(uuid -> - futures.add(dbUpgradeExecutor.submit(() -> { - try { - assetProfileService.createDefaultAssetProfile(TenantId.fromUUID(uuid)); - } catch (Exception e) {} - })) - ); + } while (pairs.hasNext()); Futures.allAsList(futures).get(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java index fb563895d5..ac02d77bc8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java @@ -24,7 +24,6 @@ import org.thingsboard.server.dao.model.sql.TenantEntity; import org.thingsboard.server.dao.model.sql.TenantInfoEntity; import java.util.List; -import java.util.Set; import java.util.UUID; /** @@ -54,8 +53,4 @@ public interface TenantRepository extends JpaRepository { @Query("SELECT t.id FROM TenantEntity t where t.tenantProfileId = :tenantProfileId") List findTenantIdsByTenantProfileId(@Param("tenantProfileId") UUID tenantProfileId); - - @Query("SELECT t.id FROM TenantEntity t WHERE t.id NOT IN :ids") - List getIdsNotIn(@Param("ids")Set ids); - } From a50fa91728bd0195bce76ec918037c818138f9da Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 9 Nov 2022 11:32:31 +0200 Subject: [PATCH 071/226] Refactoring code --- .../install/update/DefaultDataUpdateService.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index be8f1db3df..93f83414ae 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -49,7 +49,11 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.FilterPredicateValue; -import org.thingsboard.server.common.data.queue.*; +import org.thingsboard.server.common.data.queue.ProcessingStrategy; +import org.thingsboard.server.common.data.queue.ProcessingStrategyType; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.queue.SubmitStrategy; +import org.thingsboard.server.common.data.queue.SubmitStrategyType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; @@ -181,17 +185,15 @@ public class DefaultDataUpdateService implements DataUpdateService { } break; case "3.4.1": + log.info("Updating data from version 3.4.1 to 3.4.2 ..."); boolean skipAuditLogsMigration = getEnv("TB_SKIP_AUDIT_LOGS_MIGRATION", false); - boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false); - if (!skipAuditLogsMigration || !skipEdgeEventsMigration) { - log.info("Updating data from version 3.4.1 to 3.4.2 ..."); - } if (!skipAuditLogsMigration) { log.info("Starting audit logs migration. Can be skipped with TB_SKIP_AUDIT_LOGS_MIGRATION env variable set to true"); auditLogDao.migrateAuditLogs(); } else { log.info("Skipping audit logs migration"); } + boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false); if (!skipEdgeEventsMigration) { log.info("Starting edge events migration. Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true"); edgeEventDao.migrateEdgeEvents(); From e487f47d361909893a176546ba891483bf25a888 Mon Sep 17 00:00:00 2001 From: wanglei100 Date: Wed, 9 Nov 2022 17:41:06 +0800 Subject: [PATCH 072/226] =?UTF-8?q?=E5=B0=86master=E8=8B=B1=E6=96=87?= =?UTF-8?q?=E5=92=8C=E4=B8=AD=E6=96=87=E5=AF=B9=E5=BA=94=E7=94=9F=E6=88=90?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E7=BF=BB=E8=AF=91=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assets/locale/locale.constant-zh_CN.json | 2424 ++++++++--------- 1 file changed, 1212 insertions(+), 1212 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index 65eb61136f..5df7c5cabb 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -2,13 +2,13 @@ "access": { "unauthorized": "未授权", "unauthorized-access": "未授权访问", - "unauthorized-access-text": "你需要登陆才能访问这个资源!", + "unauthorized-access-text": "您需要登陆才能访问这个资源!", "access-forbidden": "禁止访问", - "access-forbidden-text": "你没有访问此位置的权限
如果你仍希望访问此位置,请尝试使用其他用户登录。", + "access-forbidden-text": "您没有访问此位置的权限
如果您仍希望访问此位置,请尝试使用其他用户登录。", "refresh-token-expired": "会话已过期", "refresh-token-failed": "无法刷新会话", "permission-denied": "权限被拒绝", - "permission-denied-text": "你没有执行此操作的权限!" + "permission-denied-text": "您没有执行此操作的权限!" }, "action": { "activate": "激活", @@ -65,19 +65,19 @@ }, "aggregation": { "aggregation": "聚合", - "function": "函数", - "limit": "限制", + "function": "数据聚合功能", + "limit": "限制数", "group-interval": "分组间隔", "min": "最小值", "max": "最大值", "avg": "平均值", "sum": "求和", "count": "计数", - "none": "空" + "none": "无" }, "admin": { - "general": "基础设置", - "general-settings": "基础设置", + "general": "基本设置", + "general-settings": "基本设置", "home-settings": "首页设置", "outgoing-mail": "发送邮件", "outgoing-mail-settings": "发送邮件设置", @@ -93,11 +93,11 @@ "smtp-host": "SMTP主机", "smtp-host-required": "SMTP主机必填。", "smtp-port": "SMTP端口", - "smtp-port-required": "你必须提供一个smtp端口。", + "smtp-port-required": "您必须提供一个smtp端口。", "smtp-port-invalid": "这看起来不是有效的smtp端口。", - "timeout-msec": "超时(毫秒)", + "timeout-msec": "超时时间(毫秒)", "timeout-required": "超时必填。", - "timeout-invalid": "超时值无效。", + "timeout-invalid": "这看起来不像有效的超时值。", "enable-tls": "启用TLS", "tls-version": "TLS版本", "enable-proxy": "启用代理", @@ -108,36 +108,36 @@ "proxy-port-range": "代理端口应在1到65535之间。", "proxy-user": "代理用户", "proxy-password": "代理密码", - "change-password": "更改密码", + "change-password": "Change password", "send-test-mail": "发送测试邮件", - "sms-provider": "SMS服务商", - "sms-provider-settings": "SMS服务商设置", - "sms-provider-type": "SMS服务商类型", - "sms-provider-type-required": "SMS服务商类型必填。", + "sms-provider": "SMS 服务商", + "sms-provider-settings": "SMS 服务商设置", + "sms-provider-type": "SMS 服务商类型", + "sms-provider-type-required": "SMS 服务商类型必填。", "sms-provider-type-aws-sns": "亚马逊社交网站", "sms-provider-type-twilio": "Twilio", "sms-provider-type-smpp": "SMPP", "aws-access-key-id": "AWS访问密钥ID", "aws-access-key-id-required": "需要访问AWS密钥ID", "aws-secret-access-key": "AWS秘密访问密钥", - "aws-secret-access-key-required": "AWS访问密钥必填", + "aws-secret-access-key-required": "AWS 访问密钥必填", "aws-region": "AWS地区", - "aws-region-required": "AWS地区必填", + "aws-region-required": "AWS 区域必填", "number-from": "发送方电话号码", "number-from-required": "发送方电话号码必填。", "number-to": "电话号码至", "number-to-required": "电话号码必填。", - "phone-number-hint": "E.164格式的电话号码,例如+19995550123", - "phone-number-hint-twilio": "E.164 格式的电话号码/电话号码的SID/消息服务SID,例如。 +19995550123/PNXXX/MGXXX", - "phone-number-pattern": "电话号码无效。应为E.164格式,例如+19995550123。", - "phone-number-pattern-twilio": "无效的电话号码。应该是E.164格式/电话号码的SID/消息服务SID,例如:+19995550123/PNXXX/MGXXX。", + "phone-number-hint": "E.164格式的手机号码,例如+19995550123", + "phone-number-hint-twilio": "Phone Number in E.164 format/Phone Number's SID/Messaging Service SID, ex. +19995550123/PNXXX/MGXXX", + "phone-number-pattern": "手机号码无效。应为E.164格式,例如+19995550123。", + "phone-number-pattern-twilio": "Invalid phone number. Should be in E.164 format/Phone Number's SID/Messaging Service SID, ex. +19995550123/PNXXX/MGXXX.", "sms-message": "短信", - "sms-message-required": "短信内容必填。", + "sms-message-required": "短消息内容必填。", "sms-message-max-length": "短信长度不能超过1600个字符", "twilio-account-sid": "Twilio帐户SID", - "twilio-account-sid-required": "Twilio 帐户的SID必填", + "twilio-account-sid-required": "Twilio 帐户的 SID 必填", "twilio-account-token": "Twilio帐户令牌", - "twilio-account-token-required": "Twilio帐户令牌必填", + "twilio-account-token-required": "Twilio 帐户令牌必填", "send-test-sms": "发送测试短信", "test-sms-sent": "测试短信发送成功!", "security-settings": "安全设置", @@ -145,57 +145,57 @@ "minimum-password-length": "最小密码长度", "minimum-password-length-required": "最小密码长度必填", "minimum-password-length-range": "最小密码长度应在5到50之间", - "minimum-uppercase-letters": "最小大写字母数", - "minimum-uppercase-letters-range": "大写字母的最小数目不能为负数", - "minimum-lowercase-letters": "最小小写字母数", - "minimum-lowercase-letters-range": "小写字母的最小数目不能为负数", - "minimum-digits": "最小位数", - "minimum-digits-range": "最小位数不能为负数", - "minimum-special-characters": "最小特殊字符数", - "minimum-special-characters-range": "特殊字符的最小数目不能为负数", + "minimum-uppercase-letters": "最少大写字母位数", + "minimum-uppercase-letters-range": "最少大写字母位数不能为负数", + "minimum-lowercase-letters": "最少小写字母位数", + "minimum-lowercase-letters-range": "最少小写字母位数不能为负数", + "minimum-digits": "最少数字位数", + "minimum-digits-range": "最少数字位数不能为负数", + "minimum-special-characters": "最少特殊字符位数", + "minimum-special-characters-range": "最少特殊字符位数不能为负数", "password-expiration-period-days": "密码有效期(天)", "password-expiration-period-days-range": "密码过期期限(天)不能为负", "password-reuse-frequency-days": "密码重用频率(天)", "password-reuse-frequency-days-range": "天内密码重用频率不能为负", - "allow-whitespace": "允许空格", + "allow-whitespace": "Allow whitespace", "general-policy": "基本策略", "max-failed-login-attempts": "登录失败之前的最大登录尝试次数", "minimum-max-failed-login-attempts-range": "登录失败次数不能为负数", "user-lockout-notification-email": "如果用户帐户锁定,请发送通知到电子邮件", "domain-name": "域名", - "domain-name-unique": "域名必须是唯一的。", + "domain-name-unique": "域名和协议必须是唯一的。", "domain-name-max-length": "域名长度不能大于256", - "error-verification-url": "域名不应包含符号“/”和“:”。例:thingsboard.io", + "error-verification-url": "域名不应包含符号 “/” 和 “:”。例:thingsboard.io", "oauth2": { "access-token-uri": "访问令牌URI", - "access-token-uri-required": "访问令牌URI必填。", + "access-token-uri-required": "访问令牌 URI 必填。", "activate-user": "激活用户", - "add-domain": "添加域名", - "delete-domain": "删除域名", - "add-provider": "添加提供程序", - "delete-provider": "删除提供程序", + "add-domain": "添加域", + "delete-domain": "删除域", + "add-provider": "添加 Provider", + "delete-provider": "删除 Provider", "allow-user-creation": "允许用户创建", "always-fullscreen": "始终全屏", "authorization-uri": "授权URI", - "authorization-uri-required": "授权URI必填。", + "authorization-uri-required": "授权 URI 必填。", "client-authentication-method": "客户端身份验证方法", "client-id": "客户端ID", - "client-id-required": "客户端ID必填。", + "client-id-required": "客户端 ID 必填。", "client-id-max-length": "客户端ID长度不能大于256", - "client-secret": "客户端密钥", - "client-secret-required": "客户端密钥必填。", + "client-secret": "客户机密", + "client-secret-required": "需要客户端密码。", "client-secret-max-length": "客户端密钥长度不能大于2049", "custom-setting": "自定义设置", "customer-name-pattern": "客户名称模式", "customer-name-pattern-max-length": "客户名称模式长度不能大于256", "default-dashboard-name": "默认仪表板名称", "default-dashboard-name-max-length": "默认仪表板名称长度不能大于256", - "delete-domain-text": "请注意:确认删除后域名和所提供程序数据将不可用。", - "delete-domain-title": "确实要删除域名'{{domainName}}'的设置吗?", - "delete-registration-text": "请注意:确认删除后提供程序数据将不可用。", - "delete-registration-title": "确实要删除提供程序'{{name}}'吗?", + "delete-domain-text": "请注意:确认后,域和所有 provider data 将不可恢复。", + "delete-domain-title": "确定要删除域 '{{domainName}}' 的设置吗?", + "delete-registration-text": "请注意:确认后 provider data 将不可恢复。", + "delete-registration-title": "确定要删除 provider '{{name}}' 吗?", "email-attribute-key": "电子邮件属性键", - "email-attribute-key-required": "电子邮件属性键必填。", + "email-attribute-key-required": "电子邮件属性密钥必填。", "email-attribute-key-max-length": "电子邮件属性键长度不能大于32", "first-name-attribute-key": "名字属性键", "first-name-attribute-key-max-length": "名字属性键长度不能大于32", @@ -204,33 +204,33 @@ "last-name-attribute-key": "姓氏属性键", "last-name-attribute-key-max-length": "姓氏属性键长度不能大于32", "login-button-icon": "登录按钮图标", - "login-button-label": "提供程序标签", - "login-button-label-placeholder": "使用$(Provider label)登录", + "login-button-label": "Provider 标签", + "login-button-label-placeholder": "使用 $(Provider label) 登录", "login-button-label-required": "标签必填。", - "login-provider": "提供程序登录", - "mapper": "属性映射", - "new-domain": "新建域名", + "login-provider": "Login provider", + "mapper": "Mapper", + "new-domain": "新建域", "oauth2": "OAuth2", "password-max-length": "密码长度不能大于256", "redirect-uri-template": "重定向URI模板", "copy-redirect-uri": "复制重定向URI", "registration-id": "注册ID", - "registration-id-required": "注册ID必填。", + "registration-id-required": "注册 ID 必填。", "registration-id-unique": "系统的注册ID必须是唯一的。", "scope": "范围", "scope-required": "范围必填。", "tenant-name-pattern": "租户名称模式", "tenant-name-pattern-required": "租户名称模式必填。", - "tenant-name-pattern-max-length": "户名称模式长度不能大于256", + "tenant-name-pattern-max-length": "租户名称模式长度不能大于256", "tenant-name-strategy": "租户名称策略", "type": "映射器类型", "uri-pattern-error": "无效的URI格式。", - "url": "URL地址", + "url": "统一资源定位地址", "url-pattern": "无效的URL格式。", - "url-required": "URL地址必填。", + "url-required": "URL 必填。", "url-max-length": "URL地址长度不能大于256", "user-info-uri": "用户信息URI", - "user-info-uri-required": "用户信息URI必填。", + "user-info-uri-required": "用户信息 URI 必填。", "username-max-length": "用户名称长度不能大于256", "user-name-attribute-name": "用户名属性键", "user-name-attribute-name-required": "用户名属性密钥必填", @@ -318,79 +318,79 @@ }, "queue-select-name": "选择队列名称", "queue-name": "名称", - "queue-name-required": "队列名称必填!", + "queue-name-required": "队列名称必填。", "queues": "队列", "queue-partitions": "分区", "queue-submit-strategy": "提交策略", "queue-processing-strategy": "处理策略", "queue-configuration": "队列配置", "repository-settings": "仓库设置", - "repository-url": "仓库地址", - "repository-url-required": "仓库地址必填。", - "default-branch": "默认分支", + "repository-url": "仓库 URL", + "repository-url-required": "仓库 URL 必填。", + "default-branch": "默认分支名称", "repository-read-only": "Read-only", - "authentication-settings": "认证设置", - "auth-method": "认证方法", - "auth-method-username-password": "密码/令牌", + "authentication-settings": "身份验证设置", + "auth-method": "身份验证方法", + "auth-method-username-password": "密码/访问令牌", "auth-method-private-key": "私钥", - "password-access-token": "密码/令牌", - "change-password-access-token": "修改密码/令牌", + "password-access-token": "密码/访问令牌", + "change-password-access-token": "更改密码/访问令牌", "private-key": "私钥", - "drop-private-key-file-or": "拖拽私钥或", - "passphrase": "密码", - "enter-passphrase": "输入密码", - "change-passphrase": "更改密码", - "check-access": "验证", - "check-repository-access-success": "验证仓库访问成功!", - "delete-repository-settings-title": "你确定要删除仓库设置吗?", - "delete-repository-settings-text": "确认删除仓库设置后版本控制功能会失效。", + "drop-private-key-file-or": "拖放私钥文件或", + "passphrase": "口令", + "enter-passphrase": "输入口令", + "change-passphrase": "更改口令", + "check-access": "检查访问权限", + "check-repository-access-success": "已成功验证仓库访问!", + "delete-repository-settings-title": "确定要删除仓库设置吗?", + "delete-repository-settings-text": "请注意:确认后,仓库设置将被删除,版本控制功能将不可用。", "auto-commit-settings": "自动提交设置", "auto-commit-entities": "自动提交实体", - "no-auto-commit-entities-prompt": "没有实体配置自动提交", - "delete-auto-commit-settings-title": "你是否确定要删除实体自动提交配置吗?", - "delete-auto-commit-settings-text": "确认删除后自动提交设置对所有实体失效。", + "no-auto-commit-entities-prompt": "没有设置自动提交的实体", + "delete-auto-commit-settings-title": "确定要删除自动提交设置吗?", + "delete-auto-commit-settings-text": "请注意:确认后,自动提交设置将被删除,所有实体的自动提交将被禁用。", "2fa": { - "2fa": "双因素认证", - "available-providers": "认证供应商", - "issuer-name": "名称", - "issuer-name-required": "名称必填。", - "max-verification-failures-before-user-lockout": "验证失败次数", - "max-verification-failures-before-user-lockout-pattern": "验证失败次数必须是一个正整数。", - "number-of-checking-attempts": "尝试检查次数", - "number-of-checking-attempts-pattern": "尝试检查次数必须是一个正整数。", - "number-of-checking-attempts-required": "需要检查的次数。", - "number-of-codes": "数字代码", - "number-of-codes-pattern": "数字代码必须是一个正整数。", - "number-of-codes-required": "数字代码必填。", - "provider": "提供程序", - "retry-verification-code-period": "重试时间(秒)", - "retry-verification-code-period-pattern": "最小时间为5秒", - "retry-verification-code-period-required": "重试时间必填。", - "total-allowed-time-for-verification": "验证时间(秒)", - "total-allowed-time-for-verification-pattern": "允许验证时间为60秒", - "total-allowed-time-for-verification-required": "允许验证时间必填。", - "use-system-two-factor-auth-settings": "使用系统双重因子验证设置", - "verification-code-check-rate-limit": "验证代码检查限制", - "verification-code-lifetime": "验证代码周期(秒)", - "verification-code-lifetime-pattern": "验证代码周期(秒)必须是一个正整数。", - "verification-code-lifetime-required": "验证代码周期(秒)必填。", + "2fa": "双因素身份验证", + "available-providers": "可用选项", + "issuer-name": "发行者名称", + "issuer-name-required": "发行者名称必填。", + "max-verification-failures-before-user-lockout": "用户锁定前最大验证失败次数", + "max-verification-failures-before-user-lockout-pattern": "最大验证失败次数必须为正整数。", + "number-of-checking-attempts": "检查尝试次数", + "number-of-checking-attempts-pattern": "检查尝试次数必须为正整数。", + "number-of-checking-attempts-required": "检查尝试次数必填。", + "number-of-codes": "验证码数量", + "number-of-codes-pattern": "验证码数量必须为正整数。", + "number-of-codes-required": "验证码数量必填。", + "provider": "Provider", + "retry-verification-code-period": "重试验证码周期(秒)", + "retry-verification-code-period-pattern": "最短时间为5秒", + "retry-verification-code-period-required": "重试验证代码周期必填。", + "total-allowed-time-for-verification": "总允许验证时间(秒)", + "total-allowed-time-for-verification-pattern": "总允许验证最小时间为60秒", + "total-allowed-time-for-verification-required": "总允许验证时间必填。", + "use-system-two-factor-auth-settings": "使用系统双因素身份验证设置", + "verification-code-check-rate-limit": "验证码检查速率限制", + "verification-code-lifetime": "验证码生存期(秒)", + "verification-code-lifetime-pattern": "验证码生存期必须为正整数。", + "verification-code-lifetime-required": "验证码生存期必填。", "verification-message-template": "验证消息模板", "verification-limitations": "验证限制", - "verification-message-template-pattern": "验证消息需要包含模式:${code}", + "verification-message-template-pattern": "验证消息需要包含模板:${code}", "verification-message-template-required": "验证消息模板必填。", - "within-time": "时间限制(秒)", - "within-time-pattern": "时间限制必须是一个正整数。", - "within-time-required": "时间限制必填。" + "within-time": "在时间内 (秒)", + "within-time-pattern": "时间必须是正整数。", + "within-time-required": "时间必填。" } }, "alarm": { - "alarm": "警报", - "alarms": "警报", - "select-alarm": "选择警报", - "no-alarms-matching": "没有找到匹配'{{entity}}'的警报", - "alarm-required": "警报必填", - "alarm-status": "警报状态", - "alarm-status-list": "报警状态列表", + "alarm": "告警", + "alarms": "告警", + "select-alarm": "选择告警", + "no-alarms-matching": "未找到匹配 '{{entity}}' 的告警", + "alarm-required": "告警必填", + "alarm-status": "告警状态", + "alarm-status-list": "告警状态列表", "any-status": "任何状态", "search-status": { "ANY": "所有", @@ -405,7 +405,7 @@ "CLEARED_UNACK": "清除未确认", "CLEARED_ACK": "清除已确认" }, - "no-alarms-prompt": "未发现警报", + "no-alarms-prompt": "未发现告警", "created-time": "创建时间", "type": "类型", "severity": "严重程度", @@ -413,7 +413,7 @@ "originator-type": "发起者类型", "details": "详情", "status": "状态", - "alarm-details": "报警详细信息", + "alarm-details": "告警详细信息", "start-time": "开始时间", "end-time": "结束时间", "ack-time": "确认时间", @@ -423,33 +423,33 @@ "severity-critical": "危险", "severity-major": "重要", "severity-minor": "次要", - "severity-warning": "警报", + "severity-warning": "警告", "severity-indeterminate": "不确定", "acknowledge": "应答", "clear": "清除", - "search": "查找警报", - "selected-alarms": "已选择{ count, plural, 1 {1个警报} other {#个警报} }", + "search": "查找告警", + "selected-alarms": "已选择 { count, plural, 1 {# 个告警} other {# 个告警} }", "no-data": "无数据显示", - "polling-interval": "警报轮询间隔(秒)", - "polling-interval-required": "警报轮询间隔必填。", + "polling-interval": "告警轮询间隔(秒)", + "polling-interval-required": "告警轮询间隔必填。", "min-polling-interval-message": "轮询间隔至少是1秒。", - "aknowledge-alarms-title": "确认{ count, plural, 1 {1个警报 } other {#个警报} }", - "aknowledge-alarms-text": "确定要确认{ count, plural, 1 {1个警报} other {# 个警报} }?", - "aknowledge-alarm-title": "确认报警", - "aknowledge-alarm-text": "确定要确认报警吗?", - "clear-alarms-title": "清除{count, plural, 1 {1个警报} other { #个警报} }", - "clear-alarms-text": "确定要清除{count, plural, 1 {1个警报} other {#个警报} }?", + "aknowledge-alarms-title": "确认 { count, plural, 1 {# 个告警} other {# 个告警} }", + "aknowledge-alarms-text": "确定要确认 { count, plural, 1 {# 个告警} other {# 个告警} }吗?", + "aknowledge-alarm-title": "确认告警", + "aknowledge-alarm-text": "确定要确认告警吗?", + "clear-alarms-title": "清除 { count, plural, 1 {# 个告警} other {# 个告警} }", + "clear-alarms-text": "确定要清除 { count, plural, 1 {# 个告警} other {# 个告警} }?", "clear-alarm-title": "清除警报", "clear-alarm-text": "确定要清除警报吗?", - "alarm-status-filter": "报警状态过滤器", - "alarm-filter": "报警过滤器", - "max-count-load": "要加载的最大报警数(0-无限制)", - "max-count-load-required": "加载的最大报警数必填。", + "alarm-status-filter": "告警状态筛选器", + "alarm-filter": "告警筛选器", + "max-count-load": "要加载的最大告警数(0-无限制)", + "max-count-load-required": "加载的最大告警数必填。", "max-count-load-error-min": "最小值为0。", "fetch-size": "获取大小", - "fetch-size-required": "获取大小必填。", + "fetch-size-required": "Fetch size 必填。", "fetch-size-error-min": "最小值为10。", - "alarm-type-list": "报警类型列表", + "alarm-type-list": "告警类型列表", "any-type": "任何类型", "search-propagated-alarms": "检索已传递的警报" }, @@ -462,38 +462,38 @@ "filter-type-single-entity": "单个实体", "filter-type-entity-list": "实体列表", "filter-type-entity-name": "实体名称", - "filter-type-entity-type": "实体类型", - "filter-type-state-entity": "仪表板实体状态", + "filter-type-entity-type": "Entity type", + "filter-type-state-entity": "仪表板状态实体", "filter-type-state-entity-description": "仪表板实体令牌状态参数", "filter-type-asset-type": "资产类型", - "filter-type-asset-type-description": "类型为'{{assetType}}'的资产", - "filter-type-asset-type-and-name-description": "类型为'{{assetType}}'且以'{{prefix}}'开头的资产", + "filter-type-asset-type-description": "类型为 '{{assetType}}' 的资产", + "filter-type-asset-type-and-name-description": "类型为 '{{assetType}}' 且以 '{{prefix}}' 开头的资产", "filter-type-device-type": "设备类型", - "filter-type-device-type-description": "类型为'{{deviceType}}'的设备", - "filter-type-device-type-and-name-description": "类型为'{{deviceType}}'且以'{{prefix}}'开头的设备", + "filter-type-device-type-description": "类型为 '{{deviceType}}' 的设备", + "filter-type-device-type-and-name-description": "类型为 '{{deviceType}}' 且以 '{{prefix}}' 开头的设备", "filter-type-entity-view-type": "实体视图类型", - "filter-type-entity-view-type-description": "类型为'{{entityView}}'的实体视图", - "filter-type-entity-view-type-and-name-description": "类型为{{entityView}}'且以'{{prefix}}'开头的实体视图", - "filter-type-edge-type": "Edge类型", - "filter-type-edge-type-description": "类型为'{{edgeType}}'的Edge", - "filter-type-edge-type-and-name-description": "类型为'{{edgeType}}'且以'{{prefix}}'开头的Edge", + "filter-type-entity-view-type-description": "类型为 '{{entityView}}' 的实体视图", + "filter-type-entity-view-type-and-name-description": "类型为 {{entityView}}' 且以 '{{prefix}}' 开头的实体视图", + "filter-type-edge-type": "边缘类型", + "filter-type-edge-type-description": "类型为 '{{edgeType}}' 的边缘", + "filter-type-edge-type-and-name-description": "类型为 '{{edgeType}}' 且以 '{{prefix}}' 开头的边缘", "filter-type-relations-query": "关联查询", - "filter-type-relations-query-description": "{{entities}}具有{{relationType}}关联{{direction}}的{{rootEntity}}", - "filter-type-asset-search-query": "资产查询", - "filter-type-asset-search-query-description": "类型为{{assetTypes}}且具有{{relationType}}关联{{direction}}的{{rootEntity}}资产", - "filter-type-device-search-query": "设备查询", - "filter-type-device-search-query-description": "类型为{{deviceTypes}}且具有{{relationType}}关联{{direction}}的{{rootEntity}}设备", - "filter-type-entity-view-search-query": "实体视图查询", - "filter-type-entity-view-search-query-description": "类型为{{entityViewTypes}}且具有{{relationType}}关联{{direction}}的{{rootEntity}}实体视图", - "filter-type-apiUsageState": "Api统计状态", - "filter-type-edge-search-query": "edge查询", - "filter-type-edge-search-query-description": "类型为{{edgeTypes}}且具有{{relationType}}关联{{direction}}的{{rootEntity}}Edge", - "entity-filter": "实体过滤器", - "resolve-multiple": "解析为多个实体", - "filter-type": "过滤器类型", - "filter-type-required": "过滤器类型必填。", - "entity-filter-no-entity-matched": "未找到符合指定筛选条件的实体。", - "no-entity-filter-specified": "没有指定实体过滤器", + "filter-type-relations-query-description": "具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的 {{entities}} ", + "filter-type-asset-search-query": "资产搜索查询", + "filter-type-asset-search-query-description": "类型为 {{assetTypes}} 且具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的资产", + "filter-type-device-search-query": "设备搜索查询", + "filter-type-device-search-query-description": "类型为 {{deviceTypes}} 且具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的设备", + "filter-type-entity-view-search-query": "实体视图搜索查询", + "filter-type-entity-view-search-query-description": "类型为 {{entityViewTypes}} 且具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的实体视图", + "filter-type-apiUsageState": "Api使用状态", + "filter-type-edge-search-query": "边缘搜索查询", + "filter-type-edge-search-query-description": "类型为 {{edgeTypes}} 且具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的边缘", + "entity-filter": "实体筛选器", + "resolve-multiple": "解析为多实体", + "filter-type": "筛选器类型", + "filter-type-required": "筛选器类型必填。", + "entity-filter-no-entity-matched": "未找到匹配指定筛选条件的实体。", + "no-entity-filter-specified": "没有指定实体筛选器", "root-state-entity": "使用仪表板状态实体作为根实体", "last-level-relation": "仅获取最后一级关联", "root-entity": "根实体", @@ -530,14 +530,14 @@ "select-asset-type": "选择资产类型", "enter-asset-type": "输入资产类型", "any-asset": "任何资产", - "no-asset-types-matching": "没有找到匹配'{{entitySubtype}}'的资产类型。", + "no-asset-types-matching": "未找到匹配 '{{entitySubtype}}' 的资产类型。", "asset-type-list-empty": "资产类型未选择。", "asset-types": "资产类型", "name": "名称", "name-required": "名称必填。", "name-max-length": "名称长度不能大于256", "label-max-length": "标签长度应小于256", - "description": "描述", + "description": "说明", "type": "类型", "type-required": "类型必填。", "details": "详情", @@ -545,32 +545,32 @@ "add-asset-text": "添加新资产", "asset-details": "资产详情", "assign-assets": "分配资产", - "assign-assets-text": "分配{ count, plural, 1 {1个资产} other {#个资产} }给客户", - "assign-asset-to-edge-title": "将资产分配给Edge", - "assign-asset-to-edge-text": "请选择要分配给Edge的资产", + "assign-assets-text": "分配 { count, plural, 1 {# 个资产} other {# 个资产} } 给客户", + "assign-asset-to-edge-title": "将资产分配给边缘", + "assign-asset-to-edge-text": "请选择要分配给边缘的资产", "delete-assets": "删除资产", "unassign-assets": "取消分配资产", - "unassign-assets-action-title": "从客户处取消分配{ count, plural, 1 {1个资产} other {#个资产} }", + "unassign-assets-action-title": "从客户处取消分配 { count, plural, 1 {# 个资产} other {# 个资产} }", "assign-new-asset": "分配新资产", - "delete-asset-title": "确定要删除资产'{{assetName}}'?", - "delete-asset-text": "请注意:确认后资产数据将无法恢复。", - "delete-assets-title": "确定要删除 { count, plural, 1 {1个资产} other {#个资产} }?", - "delete-assets-action-title": "删除 { count, plural, 1 {1个资产} other {#个资产} }", - "delete-assets-text": "请注意:确认删除后资产后数据将无法恢复。", - "make-public-asset-title": "你确定你想创建公开'{{assetName}}'资产?", - "make-public-asset-text": "确认后资产数据被公开并允许他人访问。", - "make-private-asset-title": "你确定你想创建私有'{{assetName}}'资产?", - "make-private-asset-text": "确认后资产数据被公开并允许他人访问。", - "unassign-asset-title": "你确定要取消对'{{assetName}}'资产的分配吗?", - "unassign-asset-text": "确认后资产取消分配并且客户无法访问。", + "delete-asset-title": "确定要删除资产 '{{assetName}}'吗?", + "delete-asset-text": "请注意:确认后,资产及其所有相关数据将不可恢复。", + "delete-assets-title": "确定要删除 { count, plural, 1 {# 个资产} other {# 个资产} }吗?", + "delete-assets-action-title": "删除 { count, plural, 1 {# 个资产} other {# 个资产} }", + "delete-assets-text": "请注意:确认后,所有选定的资产将被删除,所有相关的数据将不可恢复。", + "make-public-asset-title": "确定要将资产 '{{assetName}}' 设为公开吗?", + "make-public-asset-text": "确认后,资产及其所有数据将被公开并被他人访问。", + "make-private-asset-title": "确定要将资产 '{{assetName}}' 设为私有吗?", + "make-private-asset-text": "确认后,资产及其所有数据将被私有化,无法被他人访问。", + "unassign-asset-title": "确定要取消对'{{assetName}}'资产的分配吗?", + "unassign-asset-text": "确认后,资产将未分配,客户无法访问。", "unassign-asset": "未分配资产", - "unassign-assets-title": "你确定要取消分配{ count, plural, 1 {1个资产} other {#个资产} }吗?", - "unassign-assets-text": "确认后资产将取消分配并且客户无法访问。", - "unassign-assets-from-edge": "取消分配Edge", + "unassign-assets-title": "确定要取消分配 { count, plural, 1 {# 个资产} other {# 个资产} }吗?", + "unassign-assets-text": "确认后,所有选定的资产将被分配,客户无法访问。", + "unassign-assets-from-edge": "取消分配边缘", "copyId": "复制资产ID", "idCopiedMessage": "资产ID已经复制到粘贴板", "select-asset": "选择资产", - "no-assets-matching": "没有找到匹配'{{entity}}'的资产。", + "no-assets-matching": "未找到匹配 '{{entity}}' 的资产。", "asset-required": "资产必填", "name-starts-with": "资产名称以此开头", "help-text": "根据需要可以使用'%'进行匹配,例如:'%asset_name_contains%', '%asset_name_ends', 'asset_starts_with'。", @@ -578,13 +578,13 @@ "asset-file": "资产档案", "label": "标签", "search": "查找资产", - "assign-asset-to-edge": "将资产分配给Edge", - "unassign-asset-from-edge": "取消分配Edge", - "unassign-asset-from-edge-title": "你确定要取消对'{{assetName}}'资产的分配吗?", - "unassign-asset-from-edge-text": "确认后资产将取消分配并且Edge无法访问", - "unassign-assets-from-edge-title": "你确定要取消分配{ count, plural, 1 {1个资产} other {#个资产} }吗?", - "unassign-assets-from-edge-text": "确认后资产将取消分并且Edge无法访问", - "selected-assets": "已选择{ count, plural, 1 {1个资产} other {#个资产} }" + "assign-asset-to-edge": "Assign Asset(s) To Edge", + "unassign-asset-from-edge": "取消分配边缘", + "unassign-asset-from-edge-title": "确定要取消对'{{assetName}}'资产的分配吗?", + "unassign-asset-from-edge-text": "确认后,所有选定的资产将被分配,边缘无法访问。", + "unassign-assets-from-edge-title": "确定要取消分配 { count, plural, 1 {1 资产} other {# 个资产} }吗?", + "unassign-assets-from-edge-text": "确认后,所有选定的资产将被分配,边缘无法访问。", + "selected-assets": "已选择 { count, plural, 1 {# 个资产} other {# 个资产} }" }, "attribute": { "attributes": "属性", @@ -595,25 +595,25 @@ "scope-server": "服务端属性", "scope-shared": "共享属性", "add": "添加属性", - "key": "数据键", + "key": "键名", "key-max-length": "关键字长度应小于256", "last-update-time": "最后更新时间", "key-required": "属性键必填。", - "value": "数据值", + "value": "数值", "value-required": "属性值必填。", - "delete-attributes-title": "你确定要删除{ count, plural, 1 {1个属性} other {#个属性} }吗?", - "delete-attributes-text": "注意确认后所有属性都会被删除。", + "delete-attributes-title": "确定要删除 { count, plural, 1 {# 个属性} other {# 个属性} }吗?", + "delete-attributes-text": "注意,确认后所有选中的属性都会被删除。", "delete-attributes": "删除属性", "enter-attribute-value": "输入属性值", - "show-on-widget": "显示到部件", + "show-on-widget": "在部件上显示", "widget-mode": "部件模式", "next-widget": "下一个部件", "prev-widget": "上一个部件", "add-to-dashboard": "添加到仪表板", "add-widget-to-dashboard": "将部件添加到仪表板", - "selected-attributes": "已选择{ count, plural, 1 {1个属性} other {#个属性} }", - "selected-telemetry": "已选择{ count, plural, 1 {1个遥测} other {#个遥测} }", - "no-attributes-text": "找不到属性", + "selected-attributes": "已选择{ count, plural, 1 {# 个属性} other {# 个属性} }", + "selected-telemetry": "已选择 { count, plural, 1 {# 遥测} other {# 遥测} }", + "no-attributes-text": "未找到属性", "no-telemetry-text": "未找到遥测数据" }, "api-usage": { @@ -634,7 +634,7 @@ "javascript": "JavaScript", "javascript-executions": "JavaScript例外", "javascript-functions": "JavaScript函数", - "javascript-functions-daily-activity": "J每天执行的JavaScript函数", + "javascript-functions-daily-activity": "每天执行的JavaScript函数", "javascript-functions-hourly-activity": "每小时执行的JavaScript函数", "javascript-functions-monthly-activity": "每月执行的JavaScript函数", "latest-error": "最新错误", @@ -691,14 +691,14 @@ "type-attributes-updated": "更新属性", "type-attributes-deleted": "删除属性", "type-rpc-call": "RPC调用", - "type-credentials-updated": "更新凭证", + "type-credentials-updated": "更新凭据", "type-assigned-to-customer": "分配给客户", "type-unassigned-from-customer": "未分配给客户", - "type-assigned-to-edge": "分配给Edge", - "type-unassigned-from-edge": "未分配给Edge", + "type-assigned-to-edge": "分配给边缘", + "type-unassigned-from-edge": "未分配给边缘", "type-activated": "激活", "type-suspended": "暂停", - "type-credentials-read": "读取凭证", + "type-credentials-read": "读取凭据", "type-attributes-read": "读取属性", "type-relation-add-or-update": "关联已更新", "type-relation-delete": "关联已删除", @@ -711,7 +711,7 @@ "status-success": "成功", "status-failure": "失败", "audit-log-details": "审计日志详情", - "no-audit-logs-prompt": "找不到日志", + "no-audit-logs-prompt": "未找到日志", "action-data": "活动数据", "failure-details": "失败详情", "search": "查找审计日志", @@ -724,18 +724,18 @@ "type-timeseries-deleted": "遥测数据已删除" }, "confirm-on-exit": { - "message": "你有未保存的更改。确定要离开此页吗?", - "html-message": "你有未保存的更改。
确定要离开此页面吗?", + "message": "有未保存的更改,确定要离开此页吗?", + "html-message": "有未保存的更改。
确定要离开此页面吗?", "title": "未保存的更改" }, "contact": { "country": "国家", "city": "城市", - "state": "省", + "state": "州", "postal-code": "邮政编码", "postal-code-invalid": "只允许数字。", "address": "地址", - "address2": "地址2", + "address2": "地址二", "phone": "电话", "email": "电子邮件", "no-address": "无地址", @@ -757,7 +757,7 @@ "content-type": { "json": "Json", "text": "Text", - "binary": "二进制(Base64)" + "binary": "Binary (Base64)" }, "customer": { "customer": "客户", @@ -768,52 +768,52 @@ "devices": "客户设备", "entity-views": "客户实体视图", "assets": "客户资产", - "public-dashboards": "公开仪表板", - "public-devices": "公开设备", - "public-assets": "公开资产", - "public-edges": "公开Edge", - "public-entity-views": "公开实体视图", + "public-dashboards": "公共仪表板", + "public-devices": "公共设备", + "public-assets": "公共资产", + "public-edges": "公共边缘", + "public-entity-views": "公共实体视图", "add": "添加客户", "delete": "删除此客户", "manage-customer-users": "管理客户用户", "manage-customer-devices": "管理客户设备", "manage-customer-dashboards": "管理客户仪表板", - "manage-public-devices": "管理公开设备", - "manage-public-dashboards": "管理公开仪表板", + "manage-public-devices": "管理公共设备", + "manage-public-dashboards": "管理公共仪表板", "manage-customer-assets": "管理客户资产", - "manage-public-assets": "管理公开资产", - "manage-customer-edges": "管理客户Edge", - "manage-public-edges": "管理公开Edge", + "manage-public-assets": "管理公共资产", + "manage-customer-edges": "管理客户边缘", + "manage-public-edges": "管理公共边缘", "add-customer-text": "添加新客户", - "no-customers-text": "没有找到客户", + "no-customers-text": "未找到客户", "customer-details": "客户详情", - "delete-customer-title": "你确定要删除客户'{{customerTitle}}'吗?", - "delete-customer-text": "请注意:确认删除后客户数据将无法恢复。", - "delete-customers-title": "你确定要删除{ count, plural, 1 {1个客户} other {#个客户} }吗?", - "delete-customers-action-title": "删除{ count, plural, 1 {1个客户} other {#个客户} }", - "delete-customers-text": "请注意:确认删除后客户数据将无法恢复。", + "delete-customer-title": "确定要删除客户'{{customerTitle}}'吗?", + "delete-customer-text": "请注意:确认后,客户及其所有相关数据将不可恢复。", + "delete-customers-title": "确定要删除 { count, plural, 1 {# 个客户} other {# 个客户} }吗?", + "delete-customers-action-title": "删除 { count, plural, 1 {# 个客户} other {# 个客户} }", + "delete-customers-text": "请注意:确认后,所有选定的客户将被删除,所有相关数据将不可恢复。", "manage-users": "管理用户", "manage-assets": "管理资产", "manage-devices": "管理设备", "manage-dashboards": "管理仪表板", "title": "标题", "title-required": "标题必填。", - "title-max-length": "标题长度应小于256", - "description": "描述", + "title-max-length": "标题长度必须少于256个字符", + "description": "说明", "details": "详情", "events": "事件", "copyId": "复制客户ID", "idCopiedMessage": "客户ID已复制到粘贴板", "select-customer": "选择客户", - "no-customers-matching": "没有找到匹配'{{entity}}'的客户。", + "no-customers-matching": "未找到匹配 '{{entity}}' 的客户。", "customer-required": "客户必填", "select-default-customer": "选择默认的客户", "default-customer": "默认客户", - "default-customer-required": "需要默认客户才能在租户级别调试仪表板。", + "default-customer-required": "为了调试租户级别上的仪表板,需要默认客户。", "search": "查找客户", - "selected-customers": "已选择{ count, plural, 1 {1个客户} other {#个客户} }", - "edges": "客户Edge实例", - "manage-edges": "管理Edge" + "selected-customers": "已选择 { count, plural, 1 {# 个客户} other {# 个客户} }", + "edges": "客户边缘实例", + "manage-edges": "管理边缘" }, "datetime": { "date-from": "开始日期", @@ -823,13 +823,13 @@ }, "dashboard": { "dashboard": "仪表板", - "dashboards": "仪表板", + "dashboards": "仪表板库", "management": "仪表板管理", "view-dashboards": "查看仪表板", "add": "添加仪表板", "assign-dashboard-to-customer": "将仪表板分配给客户", "assign-dashboard-to-customer-text": "请选择要分配给客户的仪表板", - "assign-dashboard-to-edge-title": "将仪表板分配给Edge", + "assign-dashboard-to-edge-title": "将仪表板分配给边缘", "assign-to-customer-text": "请选择客户分配仪表板", "assign-to-customer": "分配给客户", "unassign-from-customer": "取消分配客户", @@ -838,17 +838,17 @@ "manage-assigned-customers": "管理已分配的客户", "assigned-customers": "已分配的客户", "assign-to-customers": "将仪表板分配给客户", - "assign-to-customers-text": "请选择需要分配的客户", + "assign-to-customers-text": "请选择客户指定仪表板", "unassign-from-customers": "客户未分配仪表板", - "unassign-from-customers-text": "请选择取消的客户", - "no-dashboards-text": "没有找到仪表板", + "unassign-from-customers-text": "请选择从仪表板中取消分配的客户", + "no-dashboards-text": "未找到仪表板", "no-widgets": "没有配置部件", "add-widget": "添加新的部件", "title": "标题", - "image": "缩略图", - "mobile-app-settings": "移动端设置", - "mobile-order": "移动端显示顺序", - "mobile-hide": "移动端隐藏", + "image": "仪表板图片", + "mobile-app-settings": "移动端应用设置", + "mobile-order": "移动端应用中的仪表板排序", + "mobile-hide": "在移动端应用中隐藏仪表板", "update-image": "更新缩略图", "take-screenshot": "截图", "select-widget-title": "选择部件", @@ -856,38 +856,38 @@ "select-widget-subtitle": "可用的部件类型列表", "delete": "删除仪表板", "title-required": "标题必填。", - "title-max-length": "标题长度应小于256", - "description": "描述", + "title-max-length": "标题长度必须少于256个字符", + "description": "说明", "details": "详情", "dashboard-details": "仪表板详情", "add-dashboard-text": "添加新的仪表板", "assign-dashboards": "分配仪表板", "assign-new-dashboard": "分配新的仪表板", - "assign-dashboards-text": "分配{ count, plural, 1 {1个仪表板} other {#个仪表板} }给客户", - "unassign-dashboards-action-text": "取消分配{ count, plural, 1 {1个仪表板} other {#个仪表板} }给客户", + "assign-dashboards-text": "分配 { count, plural, 1 {# 个仪表板} other {# 个仪表板} } 给客户", + "unassign-dashboards-action-text": "取消分配 { count, plural, 1 {# 个仪表板} other {# 个仪表板} } 给客户", "delete-dashboards": "删除仪表板", "unassign-dashboards": "取消分配仪表板", - "unassign-dashboards-action-title": "取消分配此客户{ count, plural, 1 {1个仪表板} other {#个仪表板} }", - "delete-dashboard-title": "你确定要删除仪表板'{{dashboardTitle}}'吗?", - "delete-dashboard-text": "请注意:确认删除后仪表板数据将无法恢复。", - "delete-dashboards-title": "确定要删除{ count, plural, 1 {1个仪表板} other {#个仪表板} }吗?", - "delete-dashboards-action-title": "删除{ count, plural, 1 {1个仪表板} other {#个仪表板} }", - "delete-dashboards-text": "请注意:确认删除后仪表板数据将无法恢复。", - "unassign-dashboard-title": "你确定要取消分配仪表板'{{dashboardTitle}}'吗?", - "unassign-dashboard-text": "确认后仪表板将取消分配,客户将无法访问。", + "unassign-dashboards-action-title": "取消分配此客户 { count, plural, 1 {# 个仪表板} other {# 个仪表板} }", + "delete-dashboard-title": "确定要删除仪表板 '{{dashboardTitle}}'吗?", + "delete-dashboard-text": "请注意:确认后,仪表板及其所有相关数据将不可恢复。", + "delete-dashboards-title": "确定要删除 { count, plural, 1 {# 个仪表板} other {# 个仪表板} }吗?", + "delete-dashboards-action-title": "删除 { count, plural, 1 {# 个仪表板} other {# 个仪表板} }", + "delete-dashboards-text": "请注意:确认后,所有选定的仪表板将被删除,所有相关数据将不可恢复。", + "unassign-dashboard-title": "确定要取消分配仪表板 '{{dashboardTitle}}'吗?", + "unassign-dashboard-text": "确认后,面板将被取消分配,客户将无法访问。", "unassign-dashboard": "取消分配仪表板", - "unassign-dashboards-title": "确定要取消分配仪表板{ count, plural, 1 {1个仪表板} other {#个仪表板} }吗?", - "unassign-dashboards-text": "确认后仪表板将取消分配,客户将无法访问。", - "public-dashboard-title": "仪表板已公开", - "public-dashboard-text": "你的仪表板{{dashboardTitle}} 已被公开,可通过如下链接访问:", - "public-dashboard-notice": "提示:不要忘记将相关设备公开以访问其数据。", - "make-private-dashboard-title": "你确定要将仪表板'{{dashboardTitle}}'设为私有吗?", - "make-private-dashboard-text": "确认后仪表板将设为私有并不允许访问。", + "unassign-dashboards-title": "确定要取消分配仪表板 { count, plural, 1 {# 个仪表板} other {# 个仪表板} } 吗?", + "unassign-dashboards-text": "确认后,所有选定的仪表板将被取消分配,客户将无法访问。", + "public-dashboard-title": "仪表板现已公开", + "public-dashboard-text": "仪表板 {{dashboardTitle}} 已被公开,可通过如下 链接访问:", + "public-dashboard-notice": "提示: 不要忘记将相关设备公开以访问其数据。", + "make-private-dashboard-title": "确定要将仪表板 '{{dashboardTitle}}' 设为私有吗?", + "make-private-dashboard-text": "确认后,仪表板将被设为私有,不能被其他人访问。", "make-private-dashboard": "仪表板设为私有", - "socialshare-text": "'{{dashboardTitle}}'由Thingsboard提供支持", - "socialshare-title": "'{{dashboardTitle}}'由Thingsboard提供支持", + "socialshare-text": "'{{dashboardTitle}}' 由Thingsboard提供支持", + "socialshare-title": "'{{dashboardTitle}}' 由Thingsboard提供支持", "select-dashboard": "选择仪表板", - "no-dashboards-matching": "找不到符合'{{entity}}'的仪表板。", + "no-dashboards-matching": "未找到匹配 '{{entity}}' 的仪表板。", "dashboard-required": "仪表板必填。", "select-existing": "选择现有仪表板", "create-new": "创建新的仪表板", @@ -897,50 +897,50 @@ "background-color": "背景颜色", "background-image": "背景图片", "background-size-mode": "背景大小模式", - "no-image": "没有图片", - "empty-image": "没有图片", - "drop-image": "拖拽图像或", + "no-image": "无图像选择", + "empty-image": "No image", + "drop-image": "拖拽图像或单击以选择要上传的文件。", "maximum-upload-file-size": "最大上传文件大小: {{ size }}", "cannot-upload-file": "无法上传文件", "settings": "设置", - "layout-settings": "布局设置", + "layout-settings": "Layout settings", "columns-count": "列数", "columns-count-required": "需要列数。", "min-columns-count-message": "只允许最少10列", "max-columns-count-message": "只允许最多1000列", "widgets-margins": "部件间边距", "margin-required": "边距值必填。", - "min-margin-message": "最小边距值为0。", - "max-margin-message": "最大边距值为50。", + "min-margin-message": "最小边距值只允许为0。", + "max-margin-message": "仅允许50作为最大边距值。", "horizontal-margin": "水平边距", - "horizontal-margin-required": "水平边距必填。", - "min-horizontal-margin-message": "最小水平边距值为0。", - "max-horizontal-margin-message": "最大水平边距值为50。", + "horizontal-margin-required": "需要水平边距值。", + "min-horizontal-margin-message": "只允许0作为最小水平边距值。", + "max-horizontal-margin-message": "只允许50作为最大水平边距值。", "vertical-margin": "垂直边距", "vertical-margin-required": "需要垂直边距值。", - "min-vertical-margin-message": "最小垂直边距值为0。", - "max-vertical-margin-message": "最大垂直边距值为50。", - "autofill-height": "自动填充高度", + "min-vertical-margin-message": "只允许0作为最小垂直边距值。", + "max-vertical-margin-message": "只允许50作为最大垂直边距值。", + "autofill-height": "自动填充布局高度", "mobile-layout": "移动端布局设置", - "mobile-row-height": "移动端行高(像素)", - "mobile-row-height-required": "移动端行高必填。", - "min-mobile-row-height-message": "移动端行高最小为5像素。", - "max-mobile-row-height-message": "移动端行高最大为200像素。", - "title-settings": "标题设置", - "display-title": "显示标题", + "mobile-row-height": "移动端行高距(px)", + "mobile-row-height-required": "移动端行高距必填。", + "min-mobile-row-height-message": "移动端行高距至少5px。", + "max-mobile-row-height-message": "移动端行高距最多200px。", + "title-settings": "Title settings", + "display-title": "显示仪表板标题", "title-color": "标题颜色", - "toolbar-settings": "工具栏设置", - "hide-toolbar": "隐藏工具栏", - "toolbar-always-open": "始终打开工具栏", + "toolbar-settings": "Toolbar settings", + "hide-toolbar": "Hide toolbar", + "toolbar-always-open": "工具栏常驻", "display-dashboards-selection": "显示仪表板选项", "display-entities-selection": "显示实体选项", - "display-filters": "显示过滤器", + "display-filters": "显示筛选器", "display-dashboard-timewindow": "显示时间窗口", "display-dashboard-export": "显示导出", "display-update-dashboard-image": "显示仪表板缩略图", "dashboard-logo-settings": "Logo设置", - "display-dashboard-logo": "全屏模式下显示Logo", - "dashboard-logo-image": "Logo图片", + "display-dashboard-logo": "在仪表板全屏模式下显示 Logo", + "dashboard-logo-image": "仪表板 Logo 图片", "advanced-settings": "高级设置", "dashboard-css": "仪表板样式", "import": "导入仪表板", @@ -948,35 +948,35 @@ "export-failed-error": "无法导出仪表板: {{error}}", "create-new-dashboard": "创建新的仪表板", "dashboard-file": "仪表板文件", - "invalid-dashboard-file-error": "仪表板数据结构无效导入失败。", + "invalid-dashboard-file-error": "无法导入仪表板: 仪表板数据结构无效。", "dashboard-import-missing-aliases-title": "配置导入仪表板使用的别名", - "create-new-widget": "添加部件", + "create-new-widget": "创建新部件", "import-widget": "导入部件", "widget-file": "部件文件", - "invalid-widget-file-error": "部件数据结构无效导入失败。", + "invalid-widget-file-error": "无法导入窗口部件: 窗口部件数据结构无效。", "widget-import-missing-aliases-title": "配置导入的窗口部件使用的别名", - "open-toolbar": "打开工具栏", + "open-toolbar": "打开仪表板工具栏", "close-toolbar": "关闭工具栏", "configuration-error": "配置错误", "alias-resolution-error-title": "仪表板别名配置错误", - "invalid-aliases-config": "无法找到与别名和过滤器匹配的设备
请联系你的管理员解决此问题。", + "invalid-aliases-config": "无法找到与某些别名筛选器匹配的任何设备。
请联系您的管理员以解决此问题。", "select-devices": "选择设备", "assignedToCustomer": "分配给客户", "assignedToCustomers": "分配给客户", "public": "公开", "copyId": "复制仪表板Id", "idCopiedMessage": "仪表板ID已经复制到粘贴板", - "public-link": "公开链接", - "copy-public-link": "复制公开链接", - "public-link-copied-message": "仪表板的公开链接已被复制到剪贴板", + "public-link": "公共链接", + "copy-public-link": "复制公共链接", + "public-link-copied-message": "仪表板的公共链接已被复制到剪贴板", "manage-states": "仪表板状态管理", "states": "仪表板状态", "search-states": "仪表板状态检索", - "selected-states": "已选择{ count, plural, 1 {1个仪表板状态} other {#个仪表板状态} }", + "selected-states": "已选择 { count, plural, 1 {# 个仪表板状态} other {# 个仪表板状态} }", "edit-state": "仪表板状态编辑", "delete-state": "删除仪表板状态", "add-state": "添加仪表板状态", - "no-states-text": "找不到状态", + "no-states-text": "未找到状态", "state": "仪表板状态", "state-name": "名称", "state-name-required": "仪表板状态名必填。", @@ -985,20 +985,20 @@ "state-id-exists": "仪表板状态ID已经存在。", "is-root-state": "根状态", "delete-state-title": "删除仪表板状态", - "delete-state-text": "确定要删除仪表板状态 '{{stateName}}' 吗?", + "delete-state-text": "确定要删除仪表板状态 '{{stateName}}' 吗?", "show-details": "显示详情", "hide-details": "隐藏详情", "select-state": "选择目标状态", "state-controller": "状态控制", "search": "查找仪表板", - "selected-dashboards": "已选择 { count, plural, 1 {1个仪表板} other {#个仪表板} }", + "selected-dashboards": "已选择 { count, plural, 1 {# 个仪表盘} other {# 个仪表盘} }", "home-dashboard": "首页仪表板", "home-dashboard-hide-toolbar": "隐藏首页仪表板工具栏", - "unassign-dashboard-from-edge-text": "确认后仪表板将取消分配,Edge将无法访问。", - "unassign-dashboards-from-edge-title": "确定要取消分配仪表板{ count, plural, 1 {1个仪表板} other {#个仪表板} }吗?", - "unassign-dashboards-from-edge-text": "确认后仪表板将取消分配,Edge将无法访问。", - "assign-dashboard-to-edge": "将仪表板分配给Edge", - "assign-dashboard-to-edge-text": "请选择要分配给Edge的仪表板", + "unassign-dashboard-from-edge-text": "确认后,所有选定的仪表板将被取消分配,边缘将无法访问。", + "unassign-dashboards-from-edge-title": "确定要取消分配仪表板 { count, plural, 1 {# 个仪表板} other {# 个仪表板} } 吗?", + "unassign-dashboards-from-edge-text": "确认后,所有选定的仪表板将被取消分配,边缘将无法访问。", + "assign-dashboard-to-edge": "将仪表板分配给边缘", + "assign-dashboard-to-edge-text": "请选择要分配给边缘的仪表板", "non-existent-dashboard-state-error": "仪表盘\"{{ stateId }}\"未找到" }, "datakey": { @@ -1008,21 +1008,21 @@ "color": "颜色", "units": "单位符号", "decimals": "小数位数", - "data-generation-func": "数据生成函数", - "use-data-post-processing-func": "数据处理函数", + "data-generation-func": "数据生成功能", + "use-data-post-processing-func": "使用数据后处理功能", "configuration": "数据键配置", - "timeseries": "时间序列", + "timeseries": "Timeseries", "attributes": "属性", "entity-field": "实体字段", - "alarm": "报警字段", - "timeseries-required": "时间序列必填。", - "timeseries-or-attributes-required": "时间序列/属性必填。", - "alarm-fields-timeseries-or-attributes-required": "警报字段或时间序列/属性必填。", - "maximum-timeseries-or-attributes": "最多允许{ count, plural, 1 {1个时间序列/属性。} other {#个时间序列/属性。}}", - "alarm-fields-required": "警报字段必填。", + "alarm": "告警字段", + "timeseries-required": "实体 Timeseries 必填。", + "timeseries-or-attributes-required": "实体 Timeseries/属性必填。", + "alarm-fields-timeseries-or-attributes-required": "告警字段或实体 Timeseries/属性必填。", + "maximum-timeseries-or-attributes": "最多允许 { count, plural, 1 {# 个 timeseries/属性。} other {# 个 timeseries/属性。} }", + "alarm-fields-required": "告警字段必填。", "function-types": "函数类型", "function-type": "函数类型", - "function-types-required": "函数类型必填。", + "function-types-required": "需要函数类型。", "alarm-keys": "报警数据键", "alarm-key": "报警数据键", "alarm-key-functions": "报警数据函数", @@ -1035,12 +1035,12 @@ "timeseries-key": "时序数据键", "timeseries-key-functions": "时序数据函数", "timeseries-key-function": "时序数据函数", - "maximum-function-types": "最多允许{ count, plural, 1 {1个函数类型} other {#个函数类型} }", + "maximum-function-types": "最多允许 { count, plural, 1 {# 个函数类型} other {# 个函数类型} }", "time-description": "当前值的时间戳;", "value-description": "当前值;", "prev-value-description": "上一次函数调用的结果;", "time-prev-description": "上一个值的时间戳;", - "prev-orig-value-description": "之前的原始值;", + "prev-orig-value-description": "先前的原始值;", "aggregation": "聚合", "aggregation-type-hint-common": "出于性能原因聚合值计算仅适用于“当前日期”、“当前月份”等固定时间间隔,而不适用于“最近30分钟”或“最近24小时”等滑动窗口间隔。", "aggregation-type-none-hint": "取最新值。", @@ -1076,17 +1076,17 @@ "management": "设备管理", "view-devices": "查看设备", "device-alias": "设备别名", - "device-type-max-length": "设备类型长度应小于256", + "device-type-max-length": "设备类型长度必须少于256个字符", "aliases": "设备别名", - "no-alias-matching": "'{{alias}}'没有找到。", - "no-aliases-found": "找不到别名。", - "no-key-matching": "'{{key}}'没有找到。", - "no-keys-found": "找不到密钥。", - "create-new-alias": "创建新的别名!", - "create-new-key": "创建新的键!", - "duplicate-alias-error": "找到重复别名'{{alias}}'。
设备别名必须是唯一的。", - "configure-alias": "配置'{{alias}}'别名", - "no-devices-matching": "找不到与'{{entity}}'匹配的设备。", + "no-alias-matching": "'{{alias}}' 未找到。", + "no-aliases-found": "未找到别名。", + "no-key-matching": "'{{key}}' 未找到。", + "no-keys-found": "未找到密钥。", + "create-new-alias": "创建一个新的!", + "create-new-key": "创建一个新的!", + "duplicate-alias-error": "找到重复别名 '{{alias}}'。
设备别名必须是唯一的。", + "configure-alias": "配置 '{{alias}}' 别名", + "no-devices-matching": "未找到与 '{{entity}}' 匹配的设备。", "alias": "别名", "alias-required": "设备别名必填。", "remove-alias": "删除设备别名", @@ -1094,19 +1094,19 @@ "name-starts-with": "名称前缀", "help-text": "根据需要可以使用'%'进行匹配,例如:'%device_name_contains%','%device_name_ends','device_starts_with'。", "device-list": "设备列表", - "use-device-name-filter": "使用过滤器", + "use-device-name-filter": "使用筛选器", "device-list-empty": "没有被选中的设备", - "device-name-filter-required": "设备名称过滤器必填。", - "device-name-filter-no-device-matched": "找不到以'{{device}}'开头的设备。", + "device-name-filter-required": "设备名称筛选器必填。", + "device-name-filter-no-device-matched": "未找到以'{{device}}' 开头的设备。", "add": "添加设备", "assign-to-customer": "分配给客户", "assign-device-to-customer": "将设备分配给客户", "assign-device-to-customer-text": "请选择要分配给客户的设备", - "assign-device-to-edge-title": "将设备分配给Edge", - "assign-device-to-edge-text": "请选择要分配给Edge的设备", + "assign-device-to-edge-title": "将设备分配给边缘", + "assign-device-to-edge-text": "请选择要分配给边缘的设备", "make-public": "公开", "make-private": "私有", - "no-devices-text": "找不到设备", + "no-devices-text": "未找到设备", "assign-to-customer-text": "请选择客户分配设备", "device-details": "设备详细信息", "add-device-text": "添加新设备", @@ -1114,38 +1114,38 @@ "manage-credentials": "管理凭据", "delete": "删除设备", "assign-devices": "分配设备", - "assign-devices-text": "将{count,plural,1 {#个设备} other {#个设备} }分配给客户", + "assign-devices-text": "将 {count,plural,1 {# 个设备} other {# 个设备} }分配给客户", "delete-devices": "删除设备", "unassign-from-customer": "取消分配客户", "unassign-devices": "取消分配设备", - "unassign-devices-action-title": "取消分配此客户的{count,plural,1 {#个设备} other {#个设备} }", - "unassign-device-from-edge-title": "你确定要取消设备'{{deviceName}}'的分配吗?", - "unassign-device-from-edge-text": "确认后分配将被取消,Edge将无法访问。", - "unassign-devices-from-edge": "取消分配Edge", + "unassign-devices-action-title": "取消分配此客户 {count,plural,1 {# 个设备} other {# 个设备} }", + "unassign-device-from-edge-title": "确定要取消分配设备 '{{deviceName}}' 吗?", + "unassign-device-from-edge-text": "确认后,设备将被取消分配,边缘将无法访问。", + "unassign-devices-from-edge": "取消分配边缘", "assign-new-device": "分配新设备", - "make-public-device-title": "你确定要将设备'{{deviceName}}'设为公开吗?", - "make-public-device-text": "确认后设备数据将公开并允许访问。", - "make-private-device-title": "你确定要将设备'{{deviceName}}'设为私有吗?", - "make-private-device-text": "确认后设备数据私有并不并允许访问。", - "view-credentials": "查看凭证", - "delete-device-title": "你确定要删除设备的{{deviceName}}吗?", - "delete-device-text": "请注意:确认删除后设备关数据将无法恢复。", - "delete-devices-title": "确定要删除{count,plural,1 {#个设备} other {#个设备} }吗?", - "delete-devices-action-title": "删除{count,plural,1 {#个设备} other {#个设备} }", - "delete-devices-text": "请注意:确认删除后设备数据将无法恢复。", - "unassign-device-title": "你确定要取消'{{deviceName}}'设备的分配吗?", - "unassign-device-text": "确认后设备分配将被取消,客户将无法访问。", + "make-public-device-title": "确定要将设备 '{{deviceName}}' 设为公开吗?", + "make-public-device-text": "确认后,设备及其所有数据将被设为公开并可被其他人访问。", + "make-private-device-title": "确定要将设备 '{{deviceName}}' 设为私有吗?", + "make-private-device-text": "确认后,设备及其所有数据将被设为私有,不被其他人访问。", + "view-credentials": "查看凭据", + "delete-device-title": "确定要删除设备的{{deviceName}}吗?", + "delete-device-text": "请注意:确认后,设备及其所有相关数据将不可恢复。", + "delete-devices-title": "确定要删除{count,plural,1 {# 个设备} other {# 个设备} } 吗?", + "delete-devices-action-title": "删除 {count,plural,1 {# 个设备} other {# 个设备} }", + "delete-devices-text": "请注意:确认后,所有选定的设备将被删除,所有相关数据将不可恢复。", + "unassign-device-title": "确定要取消分配设备 '{{deviceName}}' 吗?", + "unassign-device-text": "确认后,设备将被取消分配,客户将无法访问。", "unassign-device": "取消分配设备", - "unassign-devices-title": "确定要取消分配 {count,plural,1 {#个设备} other {#个设备} } 吗?", - "unassign-devices-text": "确认后设备分配将取消,客户将无法访问。", + "unassign-devices-title": "确定要取消分配 {count,plural,1 {# 个设备} other {# 个设备} } 吗?", + "unassign-devices-text": "确认后,所有选定的设备将被取消分配,并且客户将无法访问。", "device-credentials": "设备凭据", "loading-device-credentials": "加载设备凭证...", "credentials-type": "凭据类型", "access-token": "访问令牌", "access-token-required": "访问令牌必填", "access-token-invalid": "访问令牌长度必须为1到32个字符。", - "certificate-pem-format": "PEM证书", - "certificate-pem-format-required": "PEM证书必填。", + "certificate-pem-format": "PEM证书格式", + "certificate-pem-format-required": "PEM证书格式必填。", "lwm2m-security-config": { "identity": "客户端标识", "identity-required": "客户端标识必填。", @@ -1177,10 +1177,10 @@ "client-secret-key-tooltip-x509": "X509密钥必须是PKCS_8格式(DER编码, 标准[RFC5958])!" }, "client-id": "客户端ID", - "client-id-pattern": "客户端ID包含无效字符。", + "client-id-pattern": "包含无效字符。", "user-name": "用户名", "user-name-required": "用户名必填。", - "client-id-or-user-name-necessary": "客户端ID和用户名是必填。", + "client-id-or-user-name-necessary": "客户端ID和/或用户名是必需的", "password": "密码", "secret": "密钥", "secret-required": "密钥必填", @@ -1189,14 +1189,14 @@ "select-device-type": "选择设备类型", "enter-device-type": "输入设备类型", "any-device": "任意设备", - "no-device-types-matching": "没有找到符合'{{entitySubtype}}'的设备类型。", + "no-device-types-matching": "未找到匹配 '{{entitySubtype}}' 的设备类型。", "device-type-list-empty": "未选择设备类型", "device-types": "设备类型", "name": "名称", "name-required": "名称必填。", - "name-max-length": "名称长度不能大于256", - "label-max-length": "标签长度应小于256", - "description": "描述", + "name-max-length": "名称长度必须少于256个字符", + "label-max-length": "标签长度必须少于256个字符", + "description": "说明", "label": "标签", "events": "事件", "details": "详情", @@ -1208,7 +1208,7 @@ "mqtt-authentication-copied-message": "设备MQTT身份验证已复制到剪贴板", "assignedToCustomer": "分配给客户", "unable-delete-device-alias-title": "无法删除设备别名", - "unable-delete-device-alias-text": "设备别名'{{deviceAlias}}'不能够被删除,因为它被下列部件正在使用:
{{widgetsList}}", + "unable-delete-device-alias-text": "设备别名 '{{deviceAlias}}' 不能够被删除,因为它被下列部件所使用:
{{widgetsList}}", "is-gateway": "是否网关", "overwrite-activity-time": "覆盖已连接设备的活动时间", "public": "公开", @@ -1217,7 +1217,7 @@ "import": "导入设备", "device-file": "设备文件", "search": "查找设备", - "selected-devices": "已选择{ count, plural, 1 {1个设备} other {#个设备} }", + "selected-devices": "已选择 { count, plural, 1 {# 个设备} other {# 个设备} }", "device-configuration": "设备配置", "transport-configuration": "传输配置", "wizard": { @@ -1229,8 +1229,8 @@ "customer-to-assign-device": "客户分配设备", "add-credentials": "添加凭据" }, - "unassign-devices-from-edge-title": "你确定要取消分配{ count, plural, 1 {1个设备} other {#个设备} }吗?", - "unassign-devices-from-edge-text": "确认后设备将被取消分配,Edge将无法访问。" + "unassign-devices-from-edge-title": "确定要取消分配 { count, plural, 1 {1 个设备} other {# 个设备} } 吗?", + "unassign-devices-from-edge-text": "确认后,设备将被取消分配,边缘将无法访问。" }, "asset-profile": { "asset-profile": "资产配置", @@ -1239,41 +1239,41 @@ "add": "添加资产配置", "edit": "编辑资产配置", "asset-profile-details": "资产配置详情", - "no-asset-profiles-text": "找不到资产配置", - "search": "查找资产配置", - "selected-asset-profiles": "已选择{ count, plural, 1 {1个资产配置} other {#个资产配置} }", - "no-asset-profiles-matching": "找不到与'{{entity}}'匹配的资产配置。", + "no-asset-profiles-text": "未找到资产配置", + "search": "搜索资产配置", + "selected-asset-profiles": "已选择 { count, plural, 1 {1 个资产配置} other {# 个资产配置} }", + "no-asset-profiles-matching": "未找到与 '{{entity}}' 匹配的资产配置。", "asset-profile-required": "资产配置必填", "idCopiedMessage": "资产配置ID已复制到剪贴板", "set-default": "设为默认资产配置", "delete": "删除资产配置", "copyId": "复制资产配置ID", - "name-max-length": "名称长度不能大于256", + "name-max-length": "名称长度必须少于256个字符", "new-device-profile-name": "资产配置名称", "new-device-profile-name-required": "资产配置名称必填。", "name": "名称", - "name-required": "名称是必填的。", + "name-required": "名称必填。", "image": "资产配置图片", - "description": "描述", + "description": "说明", "default": "默认", "default-rule-chain": "默认规则链", "mobile-dashboard": "移动端仪表板", - "mobile-dashboard-hint": "移动应用程序用作资产详细信息仪表板", + "mobile-dashboard-hint": "被移动端应用用作资产详情仪表板", "select-queue-hint": "从下拉列表中选择。", - "delete-asset-profile-title": "确定要删除'{{assetProfileName}}资产配置'?", - "delete-asset-profile-text": "请注意:确认删除后资产配置数据将无法恢复。", - "delete-asset-profiles-title": "确定要删除{ count, plural, 1 {1资产配置} other {#资产配置} }?", - "delete-asset-profiles-text": "请注意:确认删除后资产配置数据将无法恢复。", - "set-default-asset-profile-title": "确实将'{{assetProfileName}}'资产配置设为默认吗?", - "set-default-asset-profile-text": "确认后资产配置将被标记为默认,并将默认指定给新资产。", - "no-asset-profiles-found": "找不到资产配置。", - "create-new-asset-profile": "创建新的资产配置!", - "create-asset-profile": "创建新的资产配置", + "delete-asset-profile-title": "确定要删除 '{{assetProfileName}}' 资产配置吗?", + "delete-asset-profile-text": "请注意:确认后,资产配置和所有相关数据将不可恢复。", + "delete-asset-profiles-title": "确定要删除 { count, plural, 1 {1 个资产配置} other {# 个资产配置} }吗?", + "delete-asset-profiles-text": "请注意:确认后,所有选定的资产配置将被删除,所有相关数据将不可恢复。", + "set-default-asset-profile-title": "确定要将 '{{assetProfileName}}' 设为默认资产配置吗?", + "set-default-asset-profile-text": "确认后,资产配置将被标记为默认,并将用于未指定配置的新资产。", + "no-asset-profiles-found": "未不到资产配置。", + "create-new-asset-profile": "创建一个新的!", + "create-asset-profile": "创建资产配置", "import": "导入资产配置", "export": "导出资产配置", - "export-failed-error": "无法导出资产配置文件: {{error}}", + "export-failed-error": "无法导出资产配置: {{error}}", "asset-profile-file": "资产配置文件", - "invalid-asset-profile-file-error": "资产配置数据结构无效导入失败。" + "invalid-asset-profile-file-error": "无法导入资产配置:无效的资产配置数据结构。" }, "device-profile": { "device-profile": "设备配置", @@ -1282,20 +1282,20 @@ "add": "添加设备配置", "edit": "编辑设备配置", "device-profile-details": "设备配置详情", - "no-device-profiles-text": "找不到设备配置", + "no-device-profiles-text": "未找到设备配置", "search": "查找设备配置", - "selected-device-profiles": "已选择{ count, plural, 1 {1个设备配置} other {#个设备配置} }", - "no-device-profiles-matching": "找不到与'{{entity}}'匹配的设备配置。", + "selected-device-profiles": "已选择 { count, plural, 1 {# 个设备配置} other {# 个设备配置} }", + "no-device-profiles-matching": "未找到与 '{{entity}}' 匹配的设备配置。", "device-profile-required": "设备配置必填", - "idCopiedMessage": "设备配置ID已复制到剪贴板", + "idCopiedMessage": "设备配置 ID 已复制到剪贴板", "set-default": "设为默认设备配置", "delete": "删除设备配置", - "copyId": "复制设备配置ID", - "name-max-length": "名称长度不能大于256", + "copyId": "复制设备配置 ID", + "name-max-length": "名称长度必须少于256个字符", "new-device-profile-name": "设备配置名称", "new-device-profile-name-required": "设备配置名称必填。", "name": "名称", - "name-required": "名称是必填的。", + "name-required": "名称是必需的。", "type": "配置类型", "type-required": "配置类型必填。", "type-default": "默认", @@ -1307,30 +1307,30 @@ "transport-type-mqtt": "MQTT", "transport-type-mqtt-hint": "启用高级MQTT传输设置", "transport-type-coap": "CoAP", - "transport-type-coap-hint": "启用高级CoAP传输设置", + "transport-type-coap-hint": "启用高级 CoAP 传输设置", "transport-type-lwm2m": "LWM2M", "transport-type-lwm2m-hint": "LWM2M传输类型", "transport-type-snmp": "SNMP", - "transport-type-snmp-hint": "指定SNMP传输配置", - "description": "描述", + "transport-type-snmp-hint": "指定 SNMP 传输配置", + "description": "说明", "default": "默认", "profile-configuration": "配置", "transport-configuration": "传输配置", "default-rule-chain": "默认规则链", "mobile-dashboard": "移动端仪表板", - "mobile-dashboard-hint": "移动应用程序用作设备详细信息仪表板", - "select-queue-hint": "从下拉列表中选择。", - "delete-device-profile-title": "确定要删除'{{deviceProfileName}}'设备配置?", - "delete-device-profile-text": "请注意:确认删除后设备配置数据将无法恢复。", - "delete-device-profiles-title": "确定要删除{ count, plural, 1 {1个设备配置} other {#个设备配置} }?", - "delete-device-profiles-text": "请注意:确认删除后设备配置数据将无法恢复。", - "set-default-device-profile-title": "确实将'{{deviceProfileName}}'设备配置设为默认吗?", - "set-default-device-profile-text": "确认后设备配置将被标记为默认,并将默认指定给新设备。", - "no-device-profiles-found": "找不到设备配置。", - "create-new-device-profile": "创建新的设备配置!", - "mqtt-device-topic-filters": "MQTT设备Topic过滤器", - "mqtt-device-topic-filters-unique": "MQTT设备Topic过滤器必须唯一。", - "mqtt-device-payload-type": "MQTT设备Payload", + "mobile-dashboard-hint": "被移动端应用用作设备详情仪表板", + "select-queue-hint": "从下拉列表中选择或添加自定义名称。", + "delete-device-profile-title": "确定要删除 '{{deviceProfileName}}' 设备配置吗?", + "delete-device-profile-text": "请注意:确认后,设备配置和所有相关数据将不可恢复。", + "delete-device-profiles-title": "确定要删除 { count, plural, 1 {# 个设备配置} other {# 个设备配置} }吗?", + "delete-device-profiles-text": "请注意:确认后,所有选定的设备配置将被删除,所有相关数据将不可恢复。", + "set-default-device-profile-title": "确定要将设备配置 '{{deviceProfileName}}' 设为默认值吗?", + "set-default-device-profile-text": "确认后,设备配置将被标记为默认,并将用于未指定配置的新设备。", + "no-device-profiles-found": "未找到设备配置。", + "create-new-device-profile": "创建一个新的!", + "mqtt-device-topic-filters": "MQTT 设备 Topic 筛选器", + "mqtt-device-topic-filters-unique": "MQTT设备 Topic 筛选器必须唯一。", + "mqtt-device-payload-type": "MQTT 设备 Payload", "mqtt-device-payload-type-json": "JSON", "mqtt-device-payload-type-proto": "Protobuf", "mqtt-enable-compatibility-with-json-payload-format": "启用与其他payload格式兼容。", @@ -1348,52 +1348,52 @@ "snmp-oid": "OID", "transport-device-payload-type-json": "JSON", "transport-device-payload-type-proto": "Protobuf", - "mqtt-payload-type-required": "Payload类型必填。", - "coap-device-type": "CoAP设备类型", - "coap-device-payload-type": "CoAP设备消息Payload", - "coap-device-type-required": "CoAP设备类型必填。", + "mqtt-payload-type-required": "Payload 类型必填。", + "coap-device-type": "CoAP 设备类型", + "coap-device-payload-type": "CoAP 设备消息 Payload", + "coap-device-type-required": "CoAP 设备类型必填。", "coap-device-type-default": "默认", "coap-device-type-efento": "Efento NB-IoT", "support-level-wildcards": "支持单[+]和多级[#]通配符。", - "telemetry-topic-filter": "遥测数据topic过滤器", - "telemetry-topic-filter-required": "遥测数据topic过滤器必填。", - "attributes-topic-filter": "属性主题过滤器", - "attributes-topic-filter-required": "属性主题过滤器必填。", - "telemetry-proto-schema": "遥测数据proto schema", - "telemetry-proto-schema-required": "遥测数据proto schema必填。", - "attributes-proto-schema": "属性proto schema", - "attributes-proto-schema-required": "属性proto schema必填。", - "rpc-response-proto-schema": "RPC响应proto schema", - "rpc-response-proto-schema-required": "RPC响应proto schema必填。", - "rpc-response-topic-filter": "RPC响应Topic过滤器", - "rpc-response-topic-filter-required": "RPC响应Topic过滤器必填。", - "rpc-request-proto-schema": "RPC请求proto schema", - "rpc-request-proto-schema-required": "RPC请求proto schema必填。", - "rpc-request-proto-schema-hint": "RPC请求消息应始终包含字段:string method = 1; int32 requestId = 2; 和params = 3的任何数据类型。", - "not-valid-pattern-topic-filter": "无效的Topic过滤器模式", + "telemetry-topic-filter": "遥测数据 topic 筛选器", + "telemetry-topic-filter-required": "遥测数据 topic 筛选器必填。", + "attributes-topic-filter": "Attributes topic filter", + "attributes-topic-filter-required": "Attributes topic 筛选器必填。", + "telemetry-proto-schema": "遥测数据 proto schema", + "telemetry-proto-schema-required": "遥测数据 proto schema 必填。", + "attributes-proto-schema": "Attributes proto schema", + "attributes-proto-schema-required": "Attributes proto schema 必填。", + "rpc-response-proto-schema": "RPC 响应 proto schema", + "rpc-response-proto-schema-required": "RPC 响应 proto schema 必填。", + "rpc-response-topic-filter": "RPC响应 Topic 筛选器", + "rpc-response-topic-filter-required": "RPC响应 Topic 筛选器必填。", + "rpc-request-proto-schema": "RPC 请求 proto schema", + "rpc-request-proto-schema-required": "RPC 请求 proto schema 必填。", + "rpc-request-proto-schema-hint": "RPC 请求消息应始终包含字段:string method = 1; int32 requestId = 2; 和params = 3的任何数据类型。", + "not-valid-pattern-topic-filter": "无效的 Topic 筛选器模式", "not-valid-single-character": "单级通配符的使用无效", "not-valid-multi-character": "多级通配符的使用无效", - "single-level-wildcards-hint": "[+] 适用于任何主题过滤,例如:v1/devices/+/telemetry或+/devices/+/attributes。", - "multi-level-wildcards-hint": "[#]可以替换topic本身,并且必须是topic的最后一个符号,例如:#或v1/devices/me/#。", - "alarm-rules": "报警规则", - "alarm-rules-with-count": "报警规则({{count}})", - "no-alarm-rules": "未配置报警规则", - "add-alarm-rule": "添加报警规则", - "edit-alarm-rule": "编辑报警规则", - "alarm-type": "报警类型", - "alarm-type-required": "报警类型必填。", + "single-level-wildcards-hint": "[+] is suitable for any topic filter level。例如:v1/devices/+/telemetry or +/devices/+/attributes。", + "multi-level-wildcards-hint": "[#]可以替换 topic filter 本身,并且必须是 topic 的最后一个符号。例如:# or v1/devices/me/#。", + "alarm-rules": "告警规则", + "alarm-rules-with-count": "告警规则 ({{count}})", + "no-alarm-rules": "未配置告警规则", + "add-alarm-rule": "添加告警规则", + "edit-alarm-rule": "编辑告警规则", + "alarm-type": "告警类型", + "alarm-type-required": "告警类型必填。", "alarm-type-unique": "警报类型在设备配置警报规则中必须唯一。", - "alarm-type-max-length": "警报类型长度必须少于256个字符", - "create-alarm-pattern": "创建{{alarmType}}报警", - "create-alarm-rules": "创建报警规则", + "alarm-type-max-length": "Alarm type should be less than 256", + "create-alarm-pattern": "创建 {{alarmType}} 告警", + "create-alarm-rules": "创建告警规则", "no-create-alarm-rules": "未配置创建条件", - "add-create-alarm-rule-prompt": "请添加创建报警规则", - "clear-alarm-rule": "清除报警规则", + "add-create-alarm-rule-prompt": "请添加创建告警规则", + "clear-alarm-rule": "清除告警规则", "no-clear-alarm-rule": "未配置明确条件", "add-create-alarm-rule": "添加创建条件", "add-clear-alarm-rule": "添加清除条件", - "select-alarm-severity": "选择报警严重性", - "alarm-severity-required": "报警严重级别必填。", + "select-alarm-severity": "选择告警严重性", + "alarm-severity-required": "告警严重级别必填。", "condition-duration": "条件持续时间", "condition-duration-value": "持续时间值", "condition-duration-time-unit": "时间单位", @@ -1408,15 +1408,15 @@ "alarm-rule-mobile-dashboard": "移动仪表盘", "alarm-rule-mobile-dashboard-hint": "用于警报详细报告", "alarm-rule-no-mobile-dashboard": "未选择仪表板", - "propagate-alarm": "向相关实体传播警报", + "propagate-alarm": "传递警报", "alarm-rule-relation-types-list": "要传递的关联类型", - "alarm-rule-relation-types-list-hint": "如果未选择传递关联类型,则将不按关联类型过滤而传递报警。", + "alarm-rule-relation-types-list-hint": "如果未选择传递关联类型,则将不按关联类型过滤而传递告警。", "propagate-alarm-to-owner": "向实体所有者(客户或租户)传播警报", "propagate-alarm-to-tenant": "向租户传播警报", - "alarm-details": "报警详细信息", - "alarm-rule-condition": "报警规则条件", - "enter-alarm-rule-condition-prompt": "请添加报警规则条件", - "edit-alarm-rule-condition": "编辑报警规则条件", + "alarm-details": "告警详细信息", + "alarm-rule-condition": "告警规则条件", + "enter-alarm-rule-condition-prompt": "请添加告警规则条件", + "edit-alarm-rule-condition": "编辑告警规则条件", "device-provisioning": "设备预配置", "provision-strategy": "预配置策略", "provision-strategy-required": "预配置策略必填。", @@ -1435,7 +1435,7 @@ "condition-type": "条件类型", "condition-type-simple": "简单", "condition-type-duration": "持续时间", - "condition-during": "在{{during}}期间", + "condition-during": "在 {{during}} 期间", "condition-during-dynamic": "在\"{{ attribute }}\" ({{during}})", "condition-type-repeating": "重复", "condition-type-required": "条件类型必填。", @@ -1443,12 +1443,12 @@ "condition-repeating-value-range": "事件计数应在1到2147483647之间。", "condition-repeating-value-pattern": "事件计数应为整数。", "condition-repeating-value-required": "事件计数值必填。", - "condition-repeat-times": "重复{ count, plural, 1 {#次} other {#次} }", - "condition-repeat-times-dynamic": "重复\"{ attribute }\" ({ count, plural, 1 {1时间} other {#时间} })", + "condition-repeat-times": "重复 { count, plural, 1 {# 次} other {# 次} }", + "condition-repeat-times-dynamic": "重复\"{ attribute }\" ({ count, plural, 1 {1 时间} other {# 时间} })", "schedule-type": "计划程序类型", "schedule-type-required": "计划类型必填。", "schedule": "启用规则:", - "edit-schedule": "编辑报警日程表", + "edit-schedule": "编辑告警日程表", "schedule-any-time": "始终启用", "schedule-specific-time": "定时启用", "schedule-custom": "自定义启用", @@ -1471,7 +1471,7 @@ "export": "导出设备配置", "export-failed-error": "无法导出设备配置文件: {{error}}", "device-profile-file": "设备配置文件", - "invalid-device-profile-file-error": "设备配置数据结构无效导入失败。", + "invalid-device-profile-file-error": "无法导入设备配置:无效的设备配置数据结构。", "power-saving-mode": "节能模式", "power-saving-mode-type": { "default": "使用设备配置的节能模式", @@ -1671,107 +1671,107 @@ "row": "排" }, "edge": { - "edge": "Edge", - "edge-instances": "Edge实例", - "edge-file": "Edge文件", + "edge": "边缘", + "edge-instances": "边缘实例", + "edge-file": "边缘文件", "name-max-length": "名称长度必须少于256个字符", "label-max-length": "标签长度必须少于256个字符", "type-max-length": "类型长度必须少于256个字符", - "management": "Edge管理", - "no-edges-matching": "没有找到匹配的Edge'{{entity}}'。", - "add": "增加Edge", - "no-edges-text": "没有找到Edge", - "edge-details": "Edge详情", - "add-edge-text": "增加新的Edge", - "delete": "删除Edge", - "delete-edge-title": "你确定删除Edge'{{edgeName}}'?", - "delete-edge-text": "请注意:确认删除后Edge以及所有关联数据将无法恢复。", - "delete-edges-title": "你确认删除Edge{ count, plural, 1 {1个Edge} other {#个Edge} }?", - "delete-edges-text": "请注意:确认删除后选定的Edge以及所有关联数据将无法恢复。", + "management": "边缘管理", + "no-edges-matching": "未找到匹配的边缘 '{{entity}}'。", + "add": "增加边缘", + "no-edges-text": "未找到边缘", + "edge-details": "边缘详情", + "add-edge-text": "增加新的边缘", + "delete": "删除边缘", + "delete-edge-title": "确定删除边缘 '{{edgeName}}'吗?", + "delete-edge-text": "当心, 确认后,边缘以及所有关联数据将不可恢复。", + "delete-edges-title": "确定删除 { count, plural, 1 {1 个边缘} other {# 个边缘} }吗?", + "delete-edges-text": "当心, 确认后,选定的边缘以及所有关联数据将不可恢复。", "name": "名称", - "name-starts-with": "Edge名称前缀", + "name-starts-with": "边缘名称前缀", "name-required": "名称必填。", - "description": "描述", + "description": "说明", "details": "详情", "events": "事件", - "copy-id": "复制Edge编号", - "id-copied-message": "Edge编号已经复制到剪切板", - "sync": "同步Edge", - "edge-required": "Edge必填。", - "edge-type": "Edge类型", - "edge-type-required": "Edge类型必填。", + "copy-id": "复制边缘编号", + "id-copied-message": "边缘编号已经复制到剪切板", + "sync": "同步边缘", + "edge-required": "边缘必填。", + "edge-type": "边缘类型", + "edge-type-required": "边缘类型必填。", "event-action": "事件行动", "entity-id": "实体编号", - "select-edge-type": "选择Edge类型", + "select-edge-type": "选择边缘类型", "assign-to-customer": "分配给客户", - "assign-to-customer-text": "请选择需要分配给Edge的客户", - "assign-edge-to-customer": "分配Edge给客户", - "assign-edge-to-customer-text": "请选择需要分配给Edge的客户", + "assign-to-customer-text": "请选择需要分配给边缘的客户", + "assign-edge-to-customer": "分配边缘给客户", + "assign-edge-to-customer-text": "请选择需要分配给边缘的客户", "assignedToCustomer": "分配给客户", - "edge-public": "Edge公开", + "edge-public": "边缘公开", "assigned-to-customer": "分配给: {{customerTitle}}", "unassign-from-customer": "取消分配客户", - "unassign-edge-title": "你确定取消分配Edge'{{edgeName}}'?", - "unassign-edge-text": "确定后Edge将被取消分配,并且客户将无法访问。", - "unassign-edges-title": "你确定要取消分配{count,plural,1 {1个Edge} other {#个Edge} }吗?", - "unassign-edges-text": "确定后所有选定的Edge将被取消分配,并且客户将无法访问。", + "unassign-edge-title": "确定取消分配边缘 '{{edgeName}}' 吗?", + "unassign-edge-text": "确定后,边缘将被取消分配,并且客户将无法访问。", + "unassign-edges-title": "确定要取消分配 {count,plural,1 {1 个边缘} other {# 个边缘} } 吗?", + "unassign-edges-text": "确定后,所有选定的边缘将被取消分配,并且客户将无法访问。", "make-public": "公开", - "make-public-edge-title": "你确定要将Edge'{{edgeName}}'设为公开吗?", - "make-public-edge-text": "确认后Edge数据将被设为公开并允许访问。", + "make-public-edge-title": "确定要将边缘 '{{edgeName}}' 设为公开吗?", + "make-public-edge-text": "确认后,边缘及其所有数据将被设为公开并可被其他人访问。", "make-private": "私有", "public": "公开", - "make-private-edge-title": "你确定要将Edge'{{edgeName}}'设为私有?", - "make-private-edge-text": "确认后Edge数据将被设为私有并不允许访问。", - "import": "导入Edge", + "make-private-edge-title": "确定要将边缘 '{{edgeName}}' 设为私有吗?", + "make-private-edge-text": "确认后,边缘及其所有数据将被设为私有,不被其他人访问。", + "import": "导入边缘", "label": "标签", "load-entity-error": "加载数据失败,实体已经被删除。", - "assign-new-edge": "分配新Edge", - "unassign-from-edge": "取消分配Edge", - "edge-key": "Edge键", - "copy-edge-key": "复制Edge键", - "edge-key-copied-message": "Edge键已经被复制到剪切板", - "edge-secret": "Edge密钥", - "copy-edge-secret": "复制Edge密钥", - "edge-secret-copied-message": "Edge密钥已经被复制到剪切板", - "edge-assets": "Edge资产", - "edge-devices": "Edge设备", - "edge-entity-views": "Edge实体视图", - "edge-dashboards": "Edge仪表板", - "edge-rulechains": "Edge规则链", - "assets": "Edge资产", - "devices": "Edge设备", - "entity-views": "Edge实体视图", - "dashboard": "Edge仪表板", - "dashboards": "Edge仪表板", + "assign-new-edge": "分配新边缘", + "unassign-from-edge": "取消分配边缘", + "edge-key": "边缘键", + "copy-edge-key": "复制边缘键", + "edge-key-copied-message": "边缘键已经被复制到剪切板", + "edge-secret": "边缘密钥", + "copy-edge-secret": "复制边缘密钥", + "edge-secret-copied-message": "边缘密钥已经被复制到剪切板", + "edge-assets": "边缘资产", + "edge-devices": "边缘设备", + "edge-entity-views": "边缘实体视图", + "edge-dashboards": "边缘仪表板", + "edge-rulechains": "边缘规则链", + "assets": "边缘资产", + "devices": "边缘设备", + "entity-views": "边缘实体视图", + "dashboard": "边缘仪表板", + "dashboards": "边缘仪表板", "rulechain-templates": "规则链模版", "rulechains": "规则链", - "search": "搜索Edge", - "selected-edges": "已选择{ count, plural, 1 {1个Edge} other {#个Edge} }", - "any-edge": "任何Edge", - "no-edge-types-matching": "没有找到匹配的Edge类型'{{entitySubtype}}'。", - "edge-type-list-empty": "没有选择Edge类型。", - "edge-types": "Edge类型", - "enter-edge-type": "输入Edge类型", + "search": "搜索边缘", + "selected-edges": "{ count, plural, 1 {1 个边缘} other {# 个边缘} } 被选中", + "any-edge": "任何边缘", + "no-edge-types-matching": "未找到匹配的边缘类型 '{{entitySubtype}}'。", + "edge-type-list-empty": "没有选择边缘类型。", + "edge-types": "边缘类型", + "enter-edge-type": "输入边缘类型", "deployed": "已部署", "pending": "待定", "downlinks": "下行", - "no-downlinks-prompt": "没有找到下行", + "no-downlinks-prompt": "未找到下行", "sync-process-started-successfully": "同步处理开始成功!", - "missing-related-rule-chains-title": "Edge缺少关联规则链", - "missing-related-rule-chains-text": "分配给Edge的规则链使用规则节点将消息转发给未分配给当前Edge的规则链。

缺少的规则链列表:
{{missingRuleChains}}", - "widget-datasource-error": "组件只支持Edge实体数据源" + "missing-related-rule-chains-title": "边缘缺少关联规则链", + "missing-related-rule-chains-text": "分配给边缘的规则链使用规则节点将消息转发给未分配给当前边缘的规则链。

缺少的规则链列表:
{{missingRuleChains}}", + "widget-datasource-error": "组件只支持边缘实体数据源" }, "edge-event": { "type-dashboard": "仪表板", "type-asset": "资产", "type-device": "设备", - "type-device-profile": "设备配置", - "type-asset-profile": "资产配置", + "type-device-profile": "设备概要", + "type-asset-profile": "Asset Profile", "type-entity-view": "实体视图", "type-alarm": "告警", "type-rule-chain": "规则链", "type-rule-chain-metadata": "规则链元数据", - "type-edge": "Edge", + "type-edge": "边缘", "type-user": "用户", "type-customer": "客户", "type-relation": "关联", @@ -1791,15 +1791,15 @@ "action-type-relation-add-or-update": "关联增加或更新", "action-type-relation-deleted": "关联删除", "action-type-rpc-call": "RPC调用", - "action-type-alarm-ack": "警报确认", - "action-type-alarm-clear": "警报清除", - "action-type-assigned-to-edge": "分配给Edge", - "action-type-unassigned-from-edge": "取消分配Edge", + "action-type-alarm-ack": "告警确认", + "action-type-alarm-clear": "告警清除", + "action-type-assigned-to-edge": "分配给边缘", + "action-type-unassigned-from-edge": "取消分配边缘", "action-type-credentials-request": "认证请求", "action-type-entity-merge-request": "实体合并请求" }, "error": { - "unable-to-connect": "无法连接到服务器,请检查你的互联网连接。", + "unable-to-connect": "无法连接到服务器!请检查您的互联网连接。", "unhandled-error-code": "未处理的错误代码: {{errorCode}}", "unknown-error": "未知错误" }, @@ -1810,10 +1810,10 @@ "aliases": "实体别名", "entity-alias": "实体别名", "unable-delete-entity-alias-title": "无法删除实体别名", - "unable-delete-entity-alias-text": "实体别名'{{entityAlias}}'被以下部件使用不能删除:
{{widgetsList}}", - "duplicate-alias-error": "别名'{{alias}}'重复。
同一仪表板别名必须唯一。", - "missing-entity-filter-error": "别名'{{alias}}'缺少过滤器", - "configure-alias": "别名'{{alias}}'配置", + "unable-delete-entity-alias-text": "实体别名 '{{entityAlias}}' 被以下部件使用不能删除:
{{widgetsList}}", + "duplicate-alias-error": "别名 '{{alias}}' 重复。
同一仪表板别名必须唯一。", + "missing-entity-filter-error": "别名 '{{alias}}' 缺少筛选器", + "configure-alias": "别名 '{{alias}}' 配置", "alias": "别名", "alias-required": "实体别名必填。", "remove-alias": "移除实体别名", @@ -1824,107 +1824,107 @@ "entity-type-list": "实体类型列表", "any-entity": "任意实体", "enter-entity-type": "输入实体类型", - "no-entities-matching": "没有找到符合'{{entity}}'的实体。", - "no-entity-types-matching": "没有找到符合'{{entityType}}'类型的实体。", + "no-entities-matching": "未找到匹配 '{{entity}}' 的实体。", + "no-entity-types-matching": "未找到匹配 '{{entityType}}' 类型的实体。", "name-starts-with": "名称开始于", "help-text": "根据需要可以使用'%'进行匹配,例如:'%entity_name_contains%', '%entity_name_ends', 'entity_starts_with'。", - "use-entity-name-filter": "用户过滤器", + "use-entity-name-filter": "用户筛选器", "entity-list-empty": "没有选择实体。", "entity-type-list-empty": "没有选择实体类型。", - "entity-name-filter-required": "实体名过滤器必填。", - "entity-name-filter-no-entity-matched": "没有找到以'{{entity}}'开头的实体", + "entity-name-filter-required": "实体名筛选器必填。", + "entity-name-filter-no-entity-matched": "未找到以 '{{entity}}' 开头的实体", "all-subtypes": "全部", "select-entities": "选择实体", - "no-aliases-found": "没有找到别名", - "no-alias-matching": "没有找到'{{alias}}'", - "create-new-alias": "创建新的别名!", - "key": "数据键", + "no-aliases-found": "未找到别名", + "no-alias-matching": "未找到 '{{alias}}'", + "create-new-alias": "创建一个新的!", + "key": "键名", "key-name": "键名", - "no-keys-found": "没有找到键", - "no-key-matching": "没有找到键'{{key}}'", - "create-new-key": "创建新的键!", + "no-keys-found": "未找到键名", + "no-key-matching": "未找到键名 '{{key}}'", + "create-new-key": "创建一个新的!", "type": "类型", "type-required": "实体类型必填。", "type-device": "设备", "type-devices": "设备", - "list-of-devices": "{ count, plural, 1 {1个设备} other {列表#个设备} }", - "device-name-starts-with": "以'{{prefix}}'开头的设备", + "list-of-devices": "{ count, plural, 1 {1 个设备} other {# 个设备} }", + "device-name-starts-with": "以 '{{prefix}}' 开头的设备", "type-device-profile": "设备配置", "type-device-profiles": "设备配置", - "list-of-device-profiles": "{ count, plural, 1 {1个设备配置} other {列表#个设备配置} }", - "device-profile-name-starts-with": "名称以'{{prefix}}'开头的设备配置", + "list-of-device-profiles": "{ count, plural, 1 {1 个设备配置} other {# 个设备配置} }", + "device-profile-name-starts-with": "名称以 '{{prefix}}' 开头的设备配置", "type-asset-profile": "资产配置", "type-asset-profiles": "资产配置", - "list-of-asset-profiles": "{ count, plural, 1 {1个资产配置} other {列表#资产配置} }", - "asset-profile-name-starts-with": "名称以'{{prefix}}'开头的资产配置", + "list-of-asset-profiles": "{ count, plural, 1 {One asset profile} other {List of # asset profiles} }", + "asset-profile-name-starts-with": "Asset profiles whose names start with '{{prefix}}'", "type-asset": "资产", "type-assets": "资产", - "list-of-assets": "{ count, plural, 1 {1个资产} other {列表#个资产} }", + "list-of-assets": "{ count, plural, 1 {1 个资产} other {# 个资产} }", "asset-name-starts-with": "以 '{{prefix}}' 开头的资产", "type-entity-view": "实体视图", "type-entity-views": "实体视图", - "list-of-entity-views": "{ count, plural, 1 {1个实体视图} other {列表#个实体视图} }", - "entity-view-name-starts-with": "以'{{prefix}}'开头的实体视图", + "list-of-entity-views": "{ count, plural, 1 {1 个实体视图} other {# 个实体视图} }", + "entity-view-name-starts-with": "以 '{{prefix}}' 开头的实体视图", "type-rule": "规则", "type-rules": "规则", - "list-of-rules": "{ count, plural, 1 {1个规则} other {列表#个规则} }", - "rule-name-starts-with": "以'{{prefix}}' 开头的规则", + "list-of-rules": "{ count, plural, 1 {1 个规则} other {# 个规则} }", + "rule-name-starts-with": "以 '{{prefix}}' 开头的规则", "type-plugin": "插件", "type-plugins": "插件", - "list-of-plugins": "{ count, plural, 1 {1个插件} other {列表#个插件} }", - "plugin-name-starts-with": "以'{{prefix}}'开头的插件", + "list-of-plugins": "{ count, plural, 1 {1 个插件} other {# 个插件} }", + "plugin-name-starts-with": "以 '{{prefix}}' 开头的插件", "type-tenant": "租户", "type-tenants": "租户", - "list-of-tenants": "{ count, plural, 1 {1个租户} other {列表#个租户} }", - "tenant-name-starts-with": "以'{{prefix}}'开头的租户", + "list-of-tenants": "{ count, plural, 1 {1 个租户} other {# 个租户} }", + "tenant-name-starts-with": "以 '{{prefix}}' 开头的租户", "type-tenant-profile": "租户简介", "type-tenant-profiles": "租户配置", - "list-of-tenant-profiles": "{ count, plural, 1 {1个租户配置} other {列表#个租户配置} }", - "tenant-profile-name-starts-with": "名称以'{{prefix}}'开头的租户配置", + "list-of-tenant-profiles": "{ count, plural, 1 {1 个租户配置} other {# 个租户配置} }", + "tenant-profile-name-starts-with": "名称以 '{{prefix}}' 开头的租户配置", "type-customer": "客户", "type-customers": "客户", - "list-of-customers": "{ count, plural, 1 {1个客户} other {列表#个客户} }", - "customer-name-starts-with": "以'{{prefix}}'开头的客户", + "list-of-customers": "{ count, plural, 1 {1 个客户} other {# 个客户} }", + "customer-name-starts-with": "以 '{{prefix}}' 开头的客户", "type-user": "用户", "type-users": "用户", - "list-of-users": "{ count, plural, 1 {1个用户} other {列表#个用户} }", - "user-name-starts-with": "以'{{prefix}}'开头的用户", + "list-of-users": "{ count, plural, 1 {1 个用户} other {# 个用户} }", + "user-name-starts-with": "以 '{{prefix}}' 开头的用户", "type-dashboard": "仪表板", "type-dashboards": "仪表板", - "list-of-dashboards": "{ count, plural, 1 {1个仪表板} other {列表#个仪表板} }", - "dashboard-name-starts-with": "以'{{prefix}}'开头的仪表板", - "type-alarm": "警报", - "type-alarms": "警报", - "list-of-alarms": "{ count, plural, 1 {1个警报} other {列表#个警报} }", - "alarm-name-starts-with": "以'{{prefix}}'开头的警报", + "list-of-dashboards": "{ count, plural, 1 {1 个仪表板} other {# 个仪表板} }", + "dashboard-name-starts-with": "以 '{{prefix}}' 开头的仪表板", + "type-alarm": "告警", + "type-alarms": "告警", + "list-of-alarms": "{ count, plural, 1 {1 个告警} other {# 个告警} }", + "alarm-name-starts-with": "以 '{{prefix}}' 开头的告警", "type-rulechain": "规则链", "type-rulechains": "规则链库", - "list-of-rulechains": "{ count, plural, 1 {1个规则链} other {列表#个规则链} }", - "rulechain-name-starts-with": "规则链前缀名称'{{prefix}}'", + "list-of-rulechains": "{ count, plural, 1 {1 个规则链} other {# 个规则链} }", + "rulechain-name-starts-with": "规则链前缀名称 '{{prefix}}'", "type-rulenode": "规则节点", "type-rulenodes": "规则节点", - "list-of-rulenodes": "{ count, plural, 1 {1个规则节点} other {列表#个规则节点} }", - "rulenode-name-starts-with": "名称以'{{prefix}}'开头的规则节点", + "list-of-rulenodes": "{ count, plural, 1 {1 个规则节点} other {# 个规则节点} }", + "rulenode-name-starts-with": "名称以 '{{prefix}}' 开头的规则节点", "type-current-customer": "当前客户", "type-current-tenant": "当前租户", "type-current-user": "当前用户", "type-current-user-owner": "当前用户所有者", - "type-widgets-bundle": "部件包", - "type-widgets-bundles": "部件包", - "list-of-widgets-bundles": "{ count, plural, 1 {1个部件包} other {列表#部件包} }", + "type-widgets-bundle": "Widgets bundle", + "type-widgets-bundles": "Widgets bundles", + "list-of-widgets-bundles": "{ count, plural, 1 {1 个部件包} other {列表 # 部件包} }", "search": "实体检索", - "selected-entities": "已选择 { count, plural, 1 {1个实体} other {#个实体} }", + "selected-entities": "已选择 { count, plural, 1 {# 个实体} other {# 个实体} }", "entity-name": "实体名", "entity-label": "实体标签", "details": "实体详情", - "no-entities-prompt": "没有找到实体", + "no-entities-prompt": "未找到实体", "no-data": "无数据", "columns-to-display": "要显示的列", - "type-api-usage-state": "Api状态统计", - "type-edge": "Edge", - "type-edges": "Edge", - "list-of-edges": "{ count, plural, 1 {1个Edge} other {列表#个Edge} }", - "edge-name-starts-with": "以'{{prefix}}'开头的Edge", + "type-api-usage-state": "Api使用状态", + "type-edge": "边缘", + "type-edges": "边缘", + "list-of-edges": "{ count, plural, 1 {1 个边缘} other {列表 # 个边缘} }", + "edge-name-starts-with": "以 '{{prefix}}' 开头的边缘", "type-tb-resource": "资源", "type-ota-package": "OTA包" }, @@ -1933,14 +1933,14 @@ "name": "名称", "type": "类型", "first-name": "名字", - "last-name": "姓", + "last-name": "姓氏", "email": "电子邮件", "title": "标题", "country": "国家", "state": "省/州", "city": "城市", "address": "地址", - "address2": "地址2", + "address2": "地址二", "zip": "邮政编码", "phone": "电话", "label": "标签" @@ -1953,76 +1953,76 @@ "view-entity-views": "查看实体视图", "entity-view-alias": "实体视图别名", "aliases": "实体视图别名", - "no-alias-matching": "'{{alias}}'没有找到。", - "no-aliases-found": "找不到别名。", - "no-key-matching": "'{{key}}'没有找到。", - "no-keys-found": "找不到密钥。", - "create-new-alias": "创建新的别名!", - "create-new-key": "创建新的键!", - "duplicate-alias-error": "找到重复别名'{{alias}}'。
实体视图别名必须是唯一的。", - "configure-alias": "配置'{{alias}}'别名", - "no-entity-views-matching": "找不到与'{{entity}}'匹配的实体视图。", + "no-alias-matching": "未找到匹配 '{{alias}}' 的别名。", + "no-aliases-found": "未找到别名。", + "no-key-matching": "'{{key}}' 未找到。", + "no-keys-found": "未找到密钥。", + "create-new-alias": "创建一个新的!", + "create-new-key": "创建一个新的!", + "duplicate-alias-error": "找到重复别名 '{{alias}}'。
实体视图别名必须是唯一的。", + "configure-alias": "配置 '{{alias}}' 别名", + "no-entity-views-matching": "未找到与 '{{entity}}' 匹配的实体视图。", "public": "公开", "alias": "别名", "alias-required": "实体视图别名必填。", - "remove-alias": "移除实体视图别名", + "remove-alias": "删除实体视图别名", "add-alias": "添加实体视图别名", "name-starts-with": "名称前缀", "help-text": "根据需要可以使用'%'进行匹配,例如:'%entity-view_name_contains%', '%entity-view_name_ends', 'entity-view_starts_with'。", "entity-view-list": "实体视图列表", - "use-entity-view-name-filter": "使用过滤器", + "use-entity-view-name-filter": "使用筛选器", "entity-view-list-empty": "没有被选中的实体视图", - "entity-view-name-filter-required": "实体视图名称过滤器必填。", - "entity-view-name-filter-no-entity-view-matched": "找不到以'{{entityView}}'开头的实体视图。", + "entity-view-name-filter-required": "实体视图名称筛选器必填。", + "entity-view-name-filter-no-entity-view-matched": "未找到以'{{entityView}}' 开头的实体视图。", "add": "添加实体视图", - "entity-view-public": "实体视图是公开的", + "entity-view-public": "实体视图是公共的", "assign-to-customer": "分配给客户", "assign-entity-view-to-customer": "将实体视图分配给客户", "assign-entity-view-to-customer-text": "请选择要分配给客户的实体视图", - "assign-entity-view-to-edge-title": "将实体视图分配给Edge", - "no-entity-views-text": "找不到实体视图", + "assign-entity-view-to-edge-title": "将实体视图分配给边缘", + "no-entity-views-text": "未找到实体视图", "assign-to-customer-text": "请选择客户分配实体视图", "entity-view-details": "实体视图详细信息", "add-entity-view-text": "添加新实体视图", "delete": "删除实体视图", "assign-entity-views": "分配实体视图", - "assign-entity-views-text": "分配{ count, plural, 1 {1个实体视图} other {#个实体视图} }给客户", + "assign-entity-views-text": "分配 { count, plural, 1 {# 个实体视图} other {# 个实体视图} } 给客户", "delete-entity-views": "删除实体视图", "unassign-from-customer": "取消分配客户", "unassign-entity-views": "取消分配实体视图", - "unassign-entity-views-action-title": "从客户处取消分配{count,plural,1 {#实体视图} other {#实体视图} }", + "unassign-entity-views-action-title": "从客户处取消分配{count,plural,1 {# 实体视图} other {# 实体视图} }", "assign-new-entity-view": "分配新实体视图", - "delete-entity-view-title": "确定要删除实体视图'{{entityViewName}}'?", - "delete-entity-view-text": "请注意:确认删除后实体视图及其所有数据将无法恢复。", - "delete-entity-views-title": "确定要删除{ count, plural, 1 {1实体视图} other {#实体视图} }?", - "delete-entity-views-action-title": "删除{ count, plural, 1 {1个实体视图} other {#个实体视图} }", - "delete-entity-views-text": "请注意:确认删除后所有选定的实体视图后,所有的数据将无法恢复。", - "unassign-entity-view-title": "你确定要取消对'{{entityViewName}}'实体视图的分配吗?", - "unassign-entity-view-text": "确认后实体视图将取消分配,客户无法访问。", - "unassign-entity-view": "取消分配实体视图", - "unassign-entity-views-title": "确定要取消分配{ count, plural, 1 {1个实体视图} other {#个实体视图} }吗?", - "unassign-entity-views-text": "确认后所选定的实体视图将被取消分配,客户无法访问。", + "delete-entity-view-title": "确定要删除实体视图 '{{entityViewName}}'吗?", + "delete-entity-view-text": "请注意:确认后实体视图及其所有相关数据将不可恢复。", + "delete-entity-views-title": "确定要删除 { count, plural, 1 {# 实体视图} other {# 实体视图} }吗?", + "delete-entity-views-action-title": "删除 { count, plural, 1 {# 个实体视图} other {# 个实体视图} }", + "delete-entity-views-text": "请注意:确认后,所有选定的实体视图将被删除,所有相关的数据将不可恢复。", + "unassign-entity-view-title": "确定要取消对 '{{entityViewName}}' 实体视图的分配吗?", + "unassign-entity-view-text": "确认后,实体视图将未分配,客户无法访问。", + "unassign-entity-view": "未分配实体视图", + "unassign-entity-views-title": "确定要取消分配 { count, plural, 1 {# 个实体视图} other {# 个实体视图} } 吗?", + "unassign-entity-views-text": "确认后,所有选定的实体视图将被分配,客户无法访问。", "entity-view-type": "实体视图类型", "entity-view-type-required": "实体视图类型必填。", "select-entity-view-type": "选择实体视图类型", "enter-entity-view-type": "输入实体视图类型", "any-entity-view": "任何实体视图", - "no-entity-view-types-matching": "没有找到匹配'{{entitySubtype}}'的实体视图类型。", + "no-entity-view-types-matching": "未找到匹配 '{{entitySubtype}}' 的实体视图类型。", "entity-view-type-list-empty": "实体视图类型未选择。", "entity-view-types": "实体视图类型", "created-time": "创建时间", "name": "名称", "name-required": "名称必填。", - "name-max-length": "名称长度不能大于256", - "type-max-length": "实体视图类型长度不能大于256", - "description": "描述", + "name-max-length": "名称长度必须少于256个字符", + "type-max-length": "视图类型长度必须少于256个字符", + "description": "说明", "events": "事件", "details": "详情", "copyId": "复制实体视图ID", - "idCopiedMessage": "实体视图Id已复制到剪贴板", + "idCopiedMessage": "实体视图ID已复制到剪贴板", "assignedToCustomer": "分配给客户", "unable-entity-view-device-alias-title": "无法删除实体视图别名", - "unable-entity-view-device-alias-text": "实体视图别名'{{entityViewAlias}}'不能够被删除,因为它被下列部件所使用:
{{widgetsList}}", + "unable-entity-view-device-alias-text": "实体视图别名 '{{entityViewAlias}}' 不能够被删除,因为它被下列部件所使用:
{{widgetsList}}", "select-entity-view": "选择实体视图", "make-public": "实体视图设为公开", "make-private": "实体视图设为私有", @@ -2043,34 +2043,34 @@ "attributes-propagation": "属性传播", "attributes-propagation-hint": "每次保存或更新这个实体视图时将自动从目标实体复制指定的属性。由于性能原因目标实体属性不会在每次属性更改时传递到实体视图。你可以通过配置\"copy to view\"规则链中的规则节点,并将\"Post attributes\"和\"attributes Updated\"消息链接到新规则节点,从而启用自动传递。", "timeseries-data": "时间序列数据", - "timeseries-data-hint": "配置目标实体的时间序列数据键,以便实体视图可以访问这些键。此时间序列数据是只读的。", + "timeseries-data-hint": "配置目标实体的 Timeseries 数据键,以便实体视图可以访问这些键。此 Timeseries 数据是只读的。", "search": "查找实体视图", - "selected-entity-views": "已选择{ count, plural, 1 {1个实体视图} other {#个实体视图} }", - "make-public-entity-view-title": "你确定你想创建公开'{{entityViewName}}'实体视图?", - "make-public-entity-view-text": "确认后实体视图数据将被公开并允许访问。", - "make-private-entity-view-title": "你确定你想创建私有'{{entityViewName}}'实体视图?", - "make-private-entity-view-text": "确认后实体视图数据将被私有并不允许访问。", - "assign-entity-view-to-edge": "将实体视图分配给Edge", - "assign-entity-view-to-edge-text": "请选择要分配给Edge的实体视图", - "unassign-entity-view-from-edge-title": "你确定要取消对'{{entityViewName}}'实体视图的分配吗?", - "unassign-entity-view-from-edge-text": "确认后实体视图将取消分配,Edge无法访问。", - "unassign-entity-views-from-edge-action-title": "从Edge处取消分配{count,plural,1 {1个实体视图} other {#个实体视图} }", + "selected-entity-views": "已选择 { count, plural, 1 {# 个实体视图} other {# 个实体视图} }", + "make-public-entity-view-title": "确定要将实体视图 '{{entityViewName}}' 设为公开吗?", + "make-public-entity-view-text": "确认后,实体视图及其所有数据将被公开并被他人访问。", + "make-private-entity-view-title": "确定要将实体视图 '{{entityViewName}}' 设为私有吗?", + "make-private-entity-view-text": "确认后,实体视图及其所有数据将被私有化,无法被他人访问。", + "assign-entity-view-to-edge": "将实体视图分配给边缘", + "assign-entity-view-to-edge-text": "请选择要分配给边缘的实体视图", + "unassign-entity-view-from-edge-title": "确定要取消对 '{{entityViewName}}' 实体视图的分配吗?", + "unassign-entity-view-from-edge-text": "确认后,实体视图将未分配,边缘无法访问。", + "unassign-entity-views-from-edge-action-title": "从边缘处取消分配{count,plural,1 {# 实体视图} other {# 实体视图} }", "unassign-entity-view-from-edge": "未分配实体视图", - "unassign-entity-views-from-edge-title": "确定要取消分配{ count, plural, 1 {1个实体视图} other {#个实体视图} }吗?", - "unassign-entity-views-from-edge-text": "确认后实体视图将被分配,Edge无法访问。" + "unassign-entity-views-from-edge-title": "确定要取消分配 { count, plural, 1 {# 个实体视图} other {# 个实体视图} } 吗?", + "unassign-entity-views-from-edge-text": "确认后,所有选定的实体视图将被分配,边缘无法访问。" }, "event": { "event-type": "事件类型", - "events-filter": "事件过滤器", - "clean-events": "清除事件", + "events-filter": "事件筛选器", + "clean-events": "Clear Events", "type-error": "错误", "type-lc-event": "生命周期事件", "type-stats": "类型统计", "type-debug-rule-node": "调试", "type-debug-rule-chain": "调试", - "no-events-prompt": "找不到事件", + "no-events-prompt": "未找到事件", "error": "错误", - "alarm": "报警", + "alarm": "告警", "event-time": "事件时间", "server": "服务器", "body": "整体", @@ -2098,7 +2098,7 @@ "all-events": "全部", "has-error": "有错误", "entity-id": "实体ID", - "copy-entity-id": "Copy entity Id", + "copy-entity-id": "复制实体ID", "entity-type": "实体类型", "clear-filter": "清除过滤器", "clear-request-title": "清除所有事件", @@ -2106,10 +2106,10 @@ }, "extension": { "extensions": "扩展", - "selected-extensions": "已选择{ count, plural, 1 {1个扩展} other {#个扩展} }", + "selected-extensions": "已选择 { count, plural, 1 {# 个扩展} other {# 个扩展} }", "type": "类型", - "key": "数据键", - "value": "数据值", + "key": "键名", + "value": "价值", "id": "ID", "extension-id": "扩展ID", "extension-type": "扩展类型", @@ -2118,10 +2118,10 @@ "delete": "删除扩展", "add": "添加扩展", "edit": "编辑扩展", - "delete-extension-title": "确实要删除扩展名'{{extensionId}}'吗?", - "delete-extension-text": "请注意:确认删除后扩展数据将无法恢复。", - "delete-extensions-title": "确定要删除{ count, plural, 1 {1个扩展} other {#个扩展} }吗?", - "delete-extensions-text": "请注意:确认删除后扩展数据将无法恢复。", + "delete-extension-title": "确定要删除扩展 '{{extensionId}}'吗?", + "delete-extension-text": "请注意:确认后,扩展和所有相关数据将不可恢复。", + "delete-extensions-title": "确定要删除 { count, plural, 1 {# 个扩展} other {# 个扩展} }吗?", + "delete-extensions-text": "请注意:确认后,所有选定的扩展将被删除,所有相关数据将不可恢复。", "converters": "转换器", "converter-id": "转换器序号", "configuration": "配置", @@ -2134,34 +2134,34 @@ "custom": "定制", "to-double": "加倍", "transformer": "转换器", - "json-required": "转换器JSON必填。", + "json-required": "Transformer JSON 必填。", "json-parse": "无法解析转换器JSON。", "attributes": "属性", "add-attribute": "添加属性", "add-map": "添加映射元素", - "timeseries": "时间序列", - "add-timeseries": "添加时间序列", + "timeseries": "Timeseries", + "add-timeseries": "添加 Timeseries", "field-required": "必填字段", "brokers": "代理服务器组", "add-broker": "添加代理服务器", "host": "主机", "port": "端口", "port-range": "端口应该在1到65535的范围内。", - "ssl": "Ssl", + "ssl": "SSL", "credentials": "证书", "username": "用户名", "password": "密码", - "retry-interval": "以毫秒为单位的重试间隔", + "retry-interval": "重试间隔(毫秒)", "anonymous": "匿名", - "basic": "基本", + "basic": "Basic", "pem": "PEM", "ca-cert": "CA certificate file *", "private-key": "Private key file *", "cert": "Certificate file *", "no-file": "没有选择文件。", - "drop-file": "拖拽图像或", + "drop-file": "删除文件或单击以选择要上载的文件。", "mapping": "映射", - "topic-filter": "Topic过滤器", + "topic-filter": "Topic筛选器", "converter-type": "转换类型", "converter-json": "Json", "json-name-expression": "设备名称JSON表达式", @@ -2177,8 +2177,8 @@ "response-topic-expression": "响应主题表达式", "value-expression": "值表达式", "topic": "主题", - "timeout": "超时ms", - "converter-json-required": "转换JSON必填。", + "timeout": "超时时间(毫秒)", + "converter-json-required": "Converter JSON 必填。", "converter-json-parse": "无法解析转换JSON。", "filter-expression": "筛选条件表达式", "connect-requests": "连接请求", @@ -2191,11 +2191,11 @@ "add-attribute-update": "添加属性更新", "server-side-rpc": "服务端RPC", "add-server-side-rpc-request": "添加服务端RPC请求", - "device-name-filter": "设备名称过滤器", - "attribute-filter": "属性过滤器", - "method-filter": "方法过滤器", + "device-name-filter": "设备名称筛选器", + "attribute-filter": "属性筛选器", + "method-filter": "方法筛选器", "request-topic-expression": "请求主题表达式", - "response-timeout": "毫秒内响应超时", + "response-timeout": "响应超时(毫秒)", "topic-expression": "主题表达", "client-scope": "客户范围", "add-device": "添加服务器", @@ -2217,14 +2217,14 @@ "opc-device-node-pattern": "设备节点模式", "opc-device-name-pattern": "设备名称模式", "modbus-server": "Servers/slaves", - "modbus-add-server": "添加server/slave", - "modbus-add-server-prompt": "请添加server/slave", - "modbus-transport": "传输", + "modbus-add-server": "添加 server/slave", + "modbus-add-server-prompt": "请添加 server/slave", + "modbus-transport": "Transport", "modbus-tcp-reconnect": "自动重新连接", "modbus-rtu-over-tcp": "RTU over TCP", "modbus-port-name": "串口名称", "modbus-encoding": "编码", - "modbus-parity": "奇偶校验", + "modbus-parity": "奇偶性", "modbus-baudrate": "波特率", "modbus-databits": "数据位", "modbus-stopbits": "停止位", @@ -2233,9 +2233,9 @@ "modbus-unit-id": "单位编号", "modbus-unit-id-range": "单位ID应该在1到247的范围内", "modbus-device-name": "设备名称", - "modbus-poll-period": "轮询周期(ms)", - "modbus-attributes-poll-period": "轮询属性周期(ms)", - "modbus-timeseries-poll-period": "时间序列轮询周期(ms)", + "modbus-poll-period": "轮询周期 (毫秒)", + "modbus-attributes-poll-period": "轮询属性周期 (毫秒)", + "modbus-timeseries-poll-period": "Timeseries 轮询周期 (毫秒)", "modbus-poll-period-range": "轮询周期应为正值。", "modbus-tag": "标签", "modbus-function": "函数", @@ -2262,24 +2262,24 @@ "invalid-file-error": "无效的扩展文件" }, "filter": { - "add": "添加过滤器", - "edit": "编辑过滤器", - "name": "过滤器名称", - "name-required": "过滤器名称必填。", - "duplicate-filter": "同名过滤器已存在。", - "filters": "过滤器", - "unable-delete-filter-title": "无法删除过滤器", - "unable-delete-filter-text": "无法删除过滤器'{{filter}}'因为它由以下小部件使用:
{{widgetsList}}", - "duplicate-filter-error": "找到重复的过滤器'{{filter}}'。
过滤器在仪表板中必须是唯一的。", - "missing-key-filters-error": "过滤器'{{filter}}'的键名筛选条件缺失。", - "filter": "过滤器", + "add": "添加筛选器", + "edit": "编辑筛选器", + "name": "筛选器名称", + "name-required": "筛选器名称必填。", + "duplicate-filter": "同名筛选器已存在。", + "filters": "筛选器", + "unable-delete-filter-title": "无法删除筛选器", + "unable-delete-filter-text": "无法删除筛选器 '{{filter}}' ,因为它由以下小部件使用:
{{widgetsList}}", + "duplicate-filter-error": "找到重复的筛选器 '{{filter}}'。
筛选器在仪表板中必须是唯一的。", + "missing-key-filters-error": "筛选器 '{{filter}}' 的键名筛选条件缺失。", + "filter": "筛选器", "editable": "可编辑", - "no-filters-found": "找不到过滤器。", - "no-filter-text": "未指定过滤器", - "add-filter-prompt": "请添加过滤器", - "no-filter-matching": "找不到'{{filter}}'。", - "create-new-filter": "创建新过滤器!", - "filter-required": "过滤器必填。", + "no-filters-found": "未找到筛选器。", + "no-filter-text": "未指定筛选器", + "add-filter-prompt": "请添加筛选器", + "no-filter-matching": "未找到 '{{filter}}' 。", + "create-new-filter": "请新增!", + "filter-required": "筛选器必填。", "operation": { "operation": "操作", "equal": "等于", @@ -2298,29 +2298,29 @@ "not-in": "不匹配" }, "ignore-case": "忽略大小写", - "value": "数据值", - "remove-filter": "移除过滤器", - "preview": "过滤器预览", - "no-filters": "未配置过滤器", - "add-filter": "添加过滤器", - "add-complex-filter": "添加复合过滤器", + "value": "值", + "remove-filter": "删除筛选器", + "preview": "筛选器预览", + "no-filters": "未配置筛选器", + "add-filter": "添加筛选器", + "add-complex-filter": "添加复合筛选器", "add-complex": "添加复合", - "complex-filter": "复合过滤器", - "edit-complex-filter": "编辑复合过滤器", - "edit-filter-user-params": "编辑过滤器谓词用户参数", + "complex-filter": "复合筛选器", + "edit-complex-filter": "编辑复合筛选器", + "edit-filter-user-params": "编辑筛选器谓词用户参数", "filter-user-params": "过滤谓词用户参数", "user-parameters": "用户参数", "display-label": "要显示的标签", "autogenerated-label": "自动生成标签", "order-priority": "字段顺序优先级", - "key-filter": "键名过滤器", - "key-filters": "键名过滤器", + "key-filter": "键名筛选器", + "key-filters": "键名筛选器", "key-name": "键名", "key-name-required": "键名必填。", "key-type": { "key-type": "键类型", "attribute": "属性", - "timeseries": "时间序列", + "timeseries": "Timeseries", "entity-field": "实体", "constant": "常量" }, @@ -2332,12 +2332,12 @@ "date-time": "日期时间" }, "value-type-required": "键值类型是必需的。", - "key-value-type-change-title": "是否确实要更改键值类型?", - "key-value-type-change-message": "如果你确认新的值类型,所有输入的键过滤器将被删除。", - "no-key-filters": "未配置键名过滤器", - "add-key-filter": "添加键名过滤器", - "remove-key-filter": "移除键名过滤器", - "edit-key-filter": "编辑键名过滤器", + "key-value-type-change-title": "确定要更改键值类型吗?", + "key-value-type-change-message": "如果您确认新的值类型,所有输入的键过滤器将被删除。", + "no-key-filters": "未配置键名筛选器", + "add-key-filter": "添加键名筛选器", + "remove-key-filter": "删除键名筛选器", + "edit-key-filter": "编辑键名筛选器", "date": "日期", "time": "时间", "current-tenant": "当前租户", @@ -2346,7 +2346,7 @@ "current-device": "当前设备", "default-value": "默认值", "dynamic-source-type": "动态源类型", - "dynamic-value": "Dynamic value", + "dynamic-value": "动态值", "no-dynamic-value": "无动态值", "source-attribute": "源属性", "switch-to-dynamic-value": "切换到动态值", @@ -2373,7 +2373,7 @@ "connector-type-required": "连接器名称类型必填。", "connectors": "连接器配置", "create-new-gateway": "创建新网关", - "create-new-gateway-text": "是否确实要创建名为'{{gatewayName}}'的新网关?", + "create-new-gateway-text": "确定要创建名为 '{{gatewayName}}' 的新网关?", "delete": "删除配置", "download-tip": "下载配置", "gateway": "网关", @@ -2385,13 +2385,13 @@ "json-required": "字段不能为空。", "no-connectors": "无连接器", "no-data": "没有配置", - "no-gateway-found": "找不到网关。", - "no-gateway-matching": "找不到'{{item}}' 。", + "no-gateway-found": "未找到网关。", + "no-gateway-matching": "未找到 '{{item}}' 。", "path-logs": "日志文件的路径", "path-logs-required": "路径是必需的。", "remote": "远程配置", "remote-logging-level": "日志记录级别", - "remove-entry": "移除配置", + "remove-entry": "删除配置", "save-tip": "保存配置", "security-type": "安全类型", "security-types": { @@ -2429,7 +2429,7 @@ "thingsboard-port-required": "端口必填。", "tidy": "整洁", "tidy-tip": "整理配置JSON", - "title-connectors-json": "连接器{{typeName}}配置", + "title-connectors-json": "连接器 {{typeName}} 配置", "tls-path-ca-certificate": "网关上CA证书的路径", "tls-path-client-certificate": "网关上客户端证书的路径", "tls-path-private-key": "网关上私钥的路径", @@ -2438,16 +2438,16 @@ "update-config": "添加/更新配置JSON" }, "grid": { - "delete-item-title": "你确定要删除此项吗?", - "delete-item-text": "注意,确认删除后此项数据将无法恢复。", - "delete-items-title": "你确定要删除{ count, plural, 1 {1条目} other {#条目} }吗?", - "delete-items-action-title": "删除{ count, plural, 1 {1个元素} other {#个元素} }", - "delete-items-text": "注意,确认删除后所有数据将无法恢复。", - "add-item-text": "添加新条目", - "no-items-text": "没有找到条目", - "item-details": "详细信息", - "delete-item": "删除条目", - "delete-items": "删除条目", + "delete-item-title": "确定要删除此项吗?", + "delete-item-text": "请注意,确认后,项目及其所有相关数据将不可恢复。", + "delete-items-title": "确定删除 { count, plural, 1 {# 项} other {# 项} }吗?", + "delete-items-action-title": "删除 { count, plural, 1 {# 个元素} other {# 个元素} }", + "delete-items-text": "请注意,确认后所有选择的项目将被删除,所有相关数据将不可恢复。", + "add-item-text": "添加新项目", + "no-items-text": "未找到项目", + "item-details": "项目详细信息", + "delete-item": "删除项目", + "delete-items": "删除项目", "scroll-to-top": "滚动到顶部" }, "help": { @@ -2474,21 +2474,21 @@ }, "import": { "no-file": "没有选择文件", - "drop-file": "拖拽文件。", + "drop-file": "拖动一个JSON文件或者单击以选择要上传的文件。", "drop-json-file-or": "拖拽JSON文件或", - "drop-file-csv": "拖拽CSV文件或", + "drop-file-csv": "拖动一个CSV文件或单击以选择要上载的文件。", "drop-file-csv-or": "拖拽CSV文件或", "column-value": "数值", "column-title": "标题", "column-example": "示例值数据", "column-key": "属性/遥测键", - "credentials": "证书", + "credentials": "Credentials", "csv-delimiter": "CSV分隔符", "csv-first-line-header": "第一行包含列名", "csv-update-data": "更新属性/遥测", - "details": "详情", + "details": "Details", "import-csv-number-columns-error": "一个文件至少应该包含两列", - "import-csv-invalid-format-error": "文件格式无效检查第'{{line}}'行数据。", + "import-csv-invalid-format-error": "文件格式无效。行: '{{line}}'", "column-type": { "name": "名称", "type": "类型", @@ -2497,7 +2497,7 @@ "client-attribute": "客户端属性", "shared-attribute": "共享属性", "server-attribute": "服务器属性", - "timeseries": "时间序列", + "timeseries": "Timeseries", "entity-field": "实体字段", "access-token": "访问令牌", "x509": "X.509", @@ -2521,9 +2521,9 @@ }, "isgateway": "是否网关", "activity-time-from-gateway-device": "网关设备活动时间", - "description": "描述", - "routing-key": "Edge键", - "secret": "Edge密钥" + "description": "说明", + "routing-key": "边缘键", + "secret": "边缘密钥" }, "stepper-text": { "select-file": "选择一个文件", @@ -2532,26 +2532,26 @@ "creat-entities": "创建新实体" }, "message": { - "create-entities": "{{count}}个新实体已成功创建。", - "update-entities": "{{count}}个实体已成功更新。", - "error-entities": "创建{{count}}个实体时出错。" + "create-entities": "{{count}} 个新实体已成功创建。", + "update-entities": "{{count}} 个实体已成功更新。", + "error-entities": "创建 {{count}} 个实体时出错。" } }, "item": { - "selected": "选定" + "selected": "选择" }, "js-func": { "no-return-error": "函数必须返回值!", - "return-type-mismatch": "函数必须返回'{{type}}'类型的值!", + "return-type-mismatch": "函数必须返回 '{{type}}' 类型的值!", "tidy": "整洁", "mini": "迷你" }, "key-val": { - "key": "数据键", - "value": "数据值", - "remove-entry": "移除", - "add-entry": "添加", - "no-data": "无数据" + "key": "键名", + "value": "数值", + "remove-entry": "删除条目", + "add-entry": "增加条目", + "no-data": "没有条目" }, "layout": { "layout": "布局", @@ -2560,22 +2560,22 @@ "color": "颜色", "main": "主体", "right": "右侧", - "left": "Left", - "select": "选择布局", - "percentage-width": "Percentage width (%)", - "fixed-width": "Fixed width (px)", - "left-width": "Left column (%)", - "right-width": "Right column (%)", - "pick-fixed-side": "Fixed side: ", - "layout-fixed-width": "Fixed width (px)", - "value-min-error": "Value must be more then {{min}}{{unit}}", - "value-max-error": "Value must be less then {{max}}{{unit}}", - "layout-fixed-width-required": "Fixed width is required", - "right-width-percentage-required": "Right percentage is required", - "left-width-percentage-required": "Left percentage is required", - "divider": "Divider", - "right-side": "Right side layout", - "left-side": "Left side layout" + "left": "左", + "select": "选择目标布局", + "percentage-width": "百分比宽度(%)", + "fixed-width": "固定(px)", + "left-width": "左列(%)", + "right-width": "右列(%)", + "pick-fixed-side": "固定侧: ", + "layout-fixed-width": "固定宽度(px)", + "value-min-error": "值是必须大于{{min}}{{unit}}", + "value-max-error": "值是必须小于{{max}}{{unit}}", + "layout-fixed-width-required": "固定宽度必填", + "right-width-percentage-required": "右侧百分比宽度必填", + "left-width-percentage-required": "左侧百分比宽度必填", + "divider": "分隔线", + "right-side": "右侧布局", + "left-side": "左侧布局" }, "legend": { "direction": "图例方向", @@ -2591,20 +2591,20 @@ "max": "最大值", "avg": "平均值", "total": "总数", - "latest": "最新值", + "latest": "latest", "comparison-time-ago": { "previousInterval": "(历史间隔)", "customInterval": "(自定义间隔)", "days": "(一天前)", "weeks": "(一周前)", - "months": "(一月前)", + "months": "(一个月前)", "years": "(一年前)" } }, "login": { "login": "登录", - "request-password-reset": "请求密码恢复", - "reset-password": "恢复密码", + "request-password-reset": "请求密码重置", + "reset-password": "重置密码", "create-password": "创建密码", "two-factor-authentication": "双重因素身份验证", "passwords-mismatch-error": "输入的密码必须相同!", @@ -2613,28 +2613,28 @@ "username": "用户名(电子邮件)", "remember-me": "记住我", "forgot-password": "忘记密码?", - "password-reset": "密码恢复", - "expired-password-reset-message": "请创建新密码凭证已失效。", + "password-reset": "密码重置", + "expired-password-reset-message": "您的凭据已过期!请创建新密码。", "new-password": "新密码", "new-password-again": "再次输入新密码", - "password-link-sent-message": "密码恢复链接已成功发送!", + "password-link-sent-message": "密码重置链接已成功发送!", "email": "电子邮件", - "login-with": "使用{{name}}登录", + "login-with": "使用 {{name}} 登录", "or": "或", "error": "登录错误", - "verify-your-identity": "身份你的验证", - "select-way-to-verify": "选择一个验证方法", - "resend-code": "重新发送验证码", - "resend-code-wait": "重新发送代码{ time, plural, 1 {1秒} other {#秒} }", - "try-another-way": "尝试另一种方式", - "totp-auth-description": "请输入你验证应用程序中的验证代码。", - "totp-auth-placeholder": "代码", - "sms-auth-description": "通过{{contact}}发送了验证码,已将其发送到你的手机。", - "sms-auth-placeholder": "SMS代码", - "email-auth-description": "{{Contact}}的验代码已发送到你的电子邮箱。", - "email-auth-placeholder": "Email代码", - "backup-code-auth-description": "请输入你的临时密码", - "backup-code-auth-placeholder": "临时密码" + "verify-your-identity": "身份验证", + "select-way-to-verify": "选择验证方式", + "resend-code": "重发验证码", + "resend-code-wait": "{ time, plural, 1 {1 秒} other {# 秒} }后重发验证码", + "try-another-way": "尝试其他方法", + "totp-auth-description": "请从验证APP中查看验证码。", + "totp-auth-placeholder": "验证码", + "sms-auth-description": "验证码已发送到手机号码 {{contact}}。", + "sms-auth-placeholder": "SMS 验证码", + "email-auth-description": "验证码已发送到电子邮箱地址 {{contact}}。", + "email-auth-placeholder": "电子邮件验证码", + "backup-code-auth-description": "请输入一个备份验证码。", + "backup-code-auth-placeholder": "备份验证码" }, "markdown": { "edit": "编辑", @@ -2648,13 +2648,13 @@ "assign-firmware-required": "分配的固件必填", "assign-software": "分配的软件", "assign-software-required": "分配的软件必填", - "auto-generate-checksum": "生成校验码", - "checksum": "校验码", + "auto-generate-checksum": "自动生成校验和", + "checksum": "校验和", "checksum-hint": "如果校验和为空,会自动生成", "checksum-algorithm": "校验和算法", "checksum-copied-message": "包校验和已复制到剪贴板", - "change-firmware": "固件的更改可能会导致{ count, plural, 1 {1个设备} other {#个设备} }的更新。", - "change-software": "软件的更改可能会导致{ count, plural, 1 {1个设备} other {#个设备} }的更新。", + "change-firmware": "固件的更改可能会导致 { count, plural, 1 {1 个设备} other {# 个设备} } 的更新。", + "change-software": "软件的更改可能会导致 { count, plural, 1 {1 个设备} other {# 个设备} } 的更新。", "chose-compatible-device-profile": "上传的包仅适用于具有所选配置文件的设备。", "chose-firmware-distributed-device": "选择将分发到设备的固件", "chose-software-distributed-device": "选择将分发到设备的软件", @@ -2664,36 +2664,36 @@ "copyId": "复制包ID", "copied": "已复制!", "delete": "删除包", - "delete-ota-update-text": "请注意:确认删除后OTA升级数据将无法恢复。", - "delete-ota-update-title": "你确定要删除OTA升级'{{title}}'吗?", - "delete-ota-updates-text": "请注意:确认删除后所有选中的OTA升级数据将无法恢复", - "delete-ota-updates-title": "你确定要删除{ count, plural, 1 {1个OTA升级} other {#个OTA升级} }吗?", - "description": "描述", + "delete-ota-update-text": "请注意:确认后,OTA升级将不可恢复。", + "delete-ota-update-title": "确定要删除 '{{title}}' OTA升级吗?", + "delete-ota-updates-text": "请注意:确认后,所有选中的OTA升级将被删除。", + "delete-ota-updates-title": "确定要删除 { count, plural, 1 {1 OTA升级} other {# OTA升级} } 吗?", + "description": "说明", "direct-url": "直接URL", "direct-url-copied-message": "包直接URL已复制到剪贴板", "direct-url-required": "直接URL必填", "download": "下载包", - "drop-file": "拖拽文件或", + "drop-file": "拖放打包文件或点击选择要上传的文件。", "drop-package-file-or": "拖拽文件或", "file-name": "文件名", "file-size": "文件大小", "file-size-bytes": "文件大小(以字节为单位)", "idCopiedMessage": "包ID已被复制到剪贴板", - "no-firmware-matching": "没有找到与'{{entity}}'匹配的兼容固件OTA升级包。", + "no-firmware-matching": "未找到与'{{entity}}'匹配的兼容固件OTA升级包。", "no-firmware-text": "没有提供兼容的固件OTA升级包。", - "no-packages-text": "没有找到包", - "no-software-matching": "没有找到匹配'{{entity}}'的兼容软件OTA升级包。", + "no-packages-text": "未找到包", + "no-software-matching": "未找到匹配 '{{entity}}' 的兼容软件OTA升级包。", "no-software-text": "没有提供兼容的软件OTA升级包。", - "ota-update": "OTA升级", - "ota-update-details": "OTA升级详情", + "ota-update": "OTA 升级", + "ota-update-details": "OTA 升级详情", "ota-updates": "OTA 升级", "package-type": "包类型", "packages-repository": "包仓库", "search": "搜索包", - "selected-package": "选中{ count, plural, 1 {1个包} other {#个包} }", + "selected-package": "{ count, plural, 1 {1 个包} other {# 个包} } 选中", "title": "标题", "title-required": "标题必填。", - "title-max-length": "标题长度应小于256", + "title-max-length": "标题长度必须少于256个字符", "types": { "firmware": "固件", "software": "软件" @@ -2703,9 +2703,9 @@ "version": "版本", "version-required": "版本必填。", "version-tag": "版本标签", - "version-tag-hint": "自定义标签应与你设备报告的软件包版本相匹配。", - "version-max-length": "版本应小于256", - "warning-after-save-no-edit": "包传后你将无法修改标题、版本、设备配置文件和包类型。" + "version-tag-hint": "自定义标签应与您设备报告的软件包版本相匹配。", + "version-max-length": "版本长度必须少于256个字符", + "warning-after-save-no-edit": "上传包后,您将无法修改标题、版本、设备配置文件和包类型。" }, "position": { "top": "顶部", @@ -2718,84 +2718,84 @@ "last-login-time": "最后登录", "change-password": "更改密码", "current-password": "当前密码", - "copy-jwt-token": "复制JWT令牌", - "jwt-token": "JWT令牌", + "copy-jwt-token": "复制 JWT 令牌", + "jwt-token": "JWT 令牌", "token-valid-till": "令牌有效期至", - "tokenCopiedSuccessMessage": "JWT令牌已复制到剪贴板", - "tokenCopiedWarnMessage": "JWT令牌已过期,请刷新页面。" + "tokenCopiedSuccessMessage": "JWT 令牌已复制到剪贴板", + "tokenCopiedWarnMessage": "JWT 令牌已过期!请刷新页面。" }, "profiles": { - "profiles": "Profiles" + "profiles": "配置" }, "security": { "security": "安全", "2fa": { - "2fa": "双因素认证", - "2fa-description": "双因素身份认证可保护您的帐户免于未经授权的访问。", - "authenticate-with": "你可以使用:", - "disable-2fa-provider-text": "禁用{{name}}你的帐户将存在安全风险", - "disable-2fa-provider-title": "你确定要禁用{{name}}吗?", - "get-new-code": "获取新代码", - "main-2fa-method": "主要用于两因素身份认证方法", + "2fa": "双因素身份验证", + "2fa-description": "双因素身份验证可保护您的帐户免受未经授权的访问。在登录时必须输入安全验证码。", + "authenticate-with": "可以使用以下身份验证:", + "disable-2fa-provider-text": "禁用 {{ name }} 会降低帐户的安全性", + "disable-2fa-provider-title": "确定要禁用 {{ name }} 吗?", + "get-new-code": "获取新验证码", + "main-2fa-method": "用作主要的双因素身份验证方法", "dialog": { - "activation-step-description-email": "下次登录时将提醒你输入邮箱中的验证码。", - "activation-step-description-sms": "下次登录时将提醒你输入短信中的验证码。", - "activation-step-description-totp": "下次登录时将提醒你输入双因素认证中的验证码。", + "activation-step-description-email": "下次登录时,系统将提示您输入电子邮件中的验证码。", + "activation-step-description-sms": "下次登录时,系统将提示您输入短信中的验证码。", + "activation-step-description-totp": "下次登录时,您需要提供一个双因素身份验证码。", "activation-step-label": "激活", - "backup-code-description": "通守临时密码在当需要时可以很方便的将其登录到你的帐户", - "backup-code-warn": "离开此页面后临时密码将无法再次显示。", + "backup-code-description": "打印出验证码,以便在您需要时使用它们登录帐户。每个备份验证码可以使用一次。", + "backup-code-warn": "离开此页后,这些代码将无法再次显示。使用以下选项安全存放。", "download-txt": "下载(txt)", - "email-step-description": "输入身份验证的电子邮件", + "email-step-description": "输入用作身份验证的电子邮件", "email-step-label": "电子邮件", - "enable-email-title": "启用电子邮件身份验证", - "enable-sms-title": "启用短信身份验证", - "enable-totp-title": "启用应用程序身份验证", + "enable-email-title": "启用电子邮件验证", + "enable-sms-title": "启用SMS验证", + "enable-totp-title": "启用验证APP", "enter-verification-code": "输入6位验证码", - "get-backup-code-title": "获取验证码", + "get-backup-code-title": "获取备份验证码", "next": "下一步", - "scan-qr-code": "通过APP扫描二维码", - "send-code": "发送代码", - "sms-step-description": "输入验证你身份器的电话号码。", - "sms-step-label": "电话号码", - "success": "成功!", - "totp-step-description-install": "你安装的Google身份验证,Authy或Duo等应用程序。", - "totp-step-description-open": "打开你手机上的身份验证应用程序。", - "totp-step-label": "获取应用程序", + "scan-qr-code": "使用验证APP扫描二维码", + "send-code": "发送验证码", + "sms-step-description": "输入用作身份验证的手机号码", + "sms-step-label": "手机号码", + "success": "操作成功!", + "totp-step-description-install": "可以安装像Google Authenticator、Authy或Duo这样的应用程序。", + "totp-step-description-open": "在手机上打开验证APP。", + "totp-step-label": "获取APP", "verification-code": "6位验证码", - "verification-code-invalid": "验证码格式错误", - "verification-code-incorrect": "验证不错误", - "verification-code-many-request": "验证码请求次数太多。", - "verification-step-description": "输入刚刚发送到{{address}}的6位验证码。", - "verification-step-label": "确认" + "verification-code-invalid": "验证码格式无效", + "verification-code-incorrect": "验证码不正确", + "verification-code-many-request": "请求过多,请检查验证码", + "verification-step-description": "输入发送到 {{ address }} 的6位代码", + "verification-step-label": "验证" }, "provider": { - "email": "邮件", - "email-description": "通过电子邮件进行身份验证。", - "email-hint": "验证码通过电子邮件发送到{{ info }}", - "sms": "短信", - "sms-description": "通过短信发送验证代码。", - "sms-hint": "验证码通过短信发送到{{ info }}", - "totp": "认证程序", - "totp-description": "通过Google认证进行身份验证。", - "totp-hint": "为你的帐户设置身份验证程序", - "backup_code": "备用验证码", - "backup-code-description": "临时密码身份验证。", - "backup-code-hint": "{{ info }}临时密码处于可用状态" + "email": "电子邮件", + "email-description": "使用您电子邮件中的验证码进行身份验证。", + "email-hint": "身份验证码通过电子邮件发送到 {{ info }}", + "sms": "SMS", + "sms-description": "使用短信进行身份验证。当登录时,系统会通过短信向您发送验证码。", + "sms-hint": "身份验证码通过短信发送到 {{ info }}", + "totp": "验证APP", + "totp-description": "使用手机上的Google Authenticator、Authy或Duo等应用程序进行身份验证,它将生成用于登录的验证码。", + "totp-hint": "已为您的帐户设置验证APP", + "backup_code": "备份验证码", + "backup-code-description": "这些可打印的一次性密码允许您在离开手机时登录,比如正在旅行。", + "backup-code-hint": "{{ info }}个一次性代码处于激活状态" } }, "password-requirement": { - "at-least": "至少", - "character": "{ count, plural, 1 {1字符} other {#字符} }", - "digit": "{ count, plural, 1 {1数字} other {#数字} }", - "incorrect-password-try-again": "密码不正确请重试。", - "lowercase-letter": "{ count, plural, 1 {1小写字母} other {#小写字母} }", + "at-least": "至少:", + "character": "{ count, plural, 1 {1 位字符} other {# 位字符} }", + "digit": "{ count, plural, 1 {1 位数字} other {# 位数字} }", + "incorrect-password-try-again": "密码不正确。再试一次", + "lowercase-letter": "{ count, plural, 1 {1 位小写字母} other {# 位小写字母} }", "new-passwords-not-match": "新密码不匹配", - "password-should-not-contain-spaces": "你的密码不应包含空格", + "password-should-not-contain-spaces": "密码不应包含空格", "password-not-meet-requirements": "密码不符合要求", "password-requirements": "密码要求", "password-should-difference": "新密码应与当前密码不同", - "special-character": "{ count, plural, 1 {1特殊字符} other {#特殊字符} }", - "uppercase-letter": "{ count, plural, 1 {1大写字母} other {#大写字母} }" + "special-character": "{ count, plural, 1 {1 位特殊字符} other {# 位特殊字符} }", + "uppercase-letter": "{ count, plural, 1 {1 位大写字母} other {# 位大写字母} }" } }, "relation": { @@ -2811,7 +2811,7 @@ }, "from-relations": "向外的关联", "to-relations": "向内的关联", - "selected-relations": "已选择{ count, plural, 1 {1个关联} other {#个关联} }", + "selected-relations": "已选择{ count, plural, 1 {# 个关联} other {# 个关联} }", "type": "类型", "to-entity-type": "到实体类型", "to-entity-name": "到实体名称", @@ -2822,22 +2822,22 @@ "delete": "删除关联", "relation-type": "关联类型", "relation-type-required": "关联类型必填", - "relation-type-max-length": "关系类型长度小于256", + "relation-type-max-length": "关联类型长度必须少于256个字符", "any-relation-type": "任何类型", "add": "添加关联", "edit": "编辑关联", - "delete-to-relation-title": "确定要删除实体'{{entityName}}'的关联吗?", - "delete-to-relation-text": "确定删除后实体'{{entityName}}'将取消与当前实体的关联关系。", - "delete-to-relations-title": "确定要删除{ count, plural, 1 {1个关联} other {#个关联} }?", + "delete-to-relation-title": "确定要删除实体 '{{entityName}}' 的关联吗?", + "delete-to-relation-text": "确定删除后实体 '{{entityName}}' 将取消与当前实体的关联关系。", + "delete-to-relations-title": "确定要删除 { count, plural, 1 {# 个关联} other {# 个关联} }吗?", "delete-to-relations-text": "确定删除所有选择的关联关系后,与当前实体对应的所有关联关系将被移除。", - "delete-from-relation-title": "确定要从实体'{{entityName}}'删除关联吗?", - "delete-from-relation-text": "确定删除后,当前实体将与实体 '{{entityName}}'取消关联", - "delete-from-relations-title": "确定删除{ count, plural, 1 {1个关联} other {#个关联} }吗?", + "delete-from-relation-title": "确定要从实体 '{{entityName}}' 删除关联吗?", + "delete-from-relation-text": "确定删除后,当前实体将与实体 '{{entityName}}' 取消关联", + "delete-from-relations-title": "确定删除 { count, plural, 1 {# 个关联} other {# 个关联} } 吗?", "delete-from-relations-text": "确定删除所有选择的关联关系后,当前实体将与对应的实体取消关联", - "remove-relation-filter": "移除关联过滤器", - "add-relation-filter": "添加关联过滤器", + "remove-relation-filter": "移除关联筛选器", + "add-relation-filter": "添加关联筛选器", "any-relation": "任意关联", - "relation-filters": "关联过滤器", + "relation-filters": "关联筛选器", "additional-info": "附加信息 (JSON)", "invalid-additional-info": "无法解析附加信息JSON。", "no-relations-text": "未找到关联" @@ -2846,49 +2846,49 @@ "add": "添加资源", "copyId": "复制资源ID", "delete": "删除资源", - "delete-resource-text": "请注意:确认删除后资源数据将无法恢复。", - "delete-resource-title": "确定要删除资源'{{resourceTitle}}'吗?", - "delete-resources-action-title": "删除{ count, plural, 1 {1个资源} other {#个资源} }", - "delete-resources-text": "请注意:确认删除后资源数据将无法恢复。", - "delete-resources-title": "确定要删除{ count, plural, 1 {1个资源} other {#个资源} }?", - "download": "下载资源", - "drop-file": "拖拽资源文件或", - "drop-resource-file-or": "拖拽资源文件或", + "delete-resource-text": "请注意:确认后,资源将不可恢复。", + "delete-resource-title": "确定要删除资源 '{{resourceTitle}}' 吗?", + "delete-resources-action-title": "删除 { count, plural, 1 {# 个资源} other {# 个资源} }", + "delete-resources-text": "请注意:确认后,所有选定的资源将被删除。", + "delete-resources-title": "确定要删除 { count, plural, 1 {# 个资源} other {# 个资源} }吗?", + "download": "Download resource", + "drop-file": "拖拽资源文件或单击以选择要上传的文件。", + "drop-resource-file-or": "Drag and drop a resource file or", "empty": "资源为空", - "file-name": "文件名", - "idCopiedMessage": "资源ID已经复制到粘贴板", - "no-resource-matching": "找不到与'{{widgetsBundle}}'匹配的资源。", - "no-resource-text": "找不到资源", + "file-name": "File name", + "idCopiedMessage": "拖拽资源文件或", + "no-resource-matching": "未找到与 '{{widgetsBundle}}' 匹配的资源。", + "no-resource-text": "未找到资源", "open-widgets-bundle": "打开部件库", "resource": "资源", "resource-library-details": "资源库详情", "resource-type": "资源类型", "resources-library": "资源库", "search": "查找资源", - "selected-resources": "已选择{ count, plural, 1 {1个资源} other {#个资源} }", + "selected-resources": "已选择{ count, plural, 1 {# 个资源} other {# 个资源} }", "system": "系统", "title": "标题", "title-required": "标题是必填项。", - "title-max-length": "标题长度小于256" + "title-max-length": "标题长度必须少于256个字符" }, "rulechain": { "rulechain": "规则链", - "rulechains": "规则链", + "rulechains": "规则链库", "root": "是否根链", "delete": "删除规则链", "name": "名称", "name-required": "名称必填。", - "name-max-length": "名称长度不能大于256", - "description": "描述", + "name-max-length": "名称长度必须少于256个字符", + "description": "说明", "add": "添加规则链", "set-root": "设置为根规则链", - "set-root-rulechain-title": "你确定要生成规则链'{{ruleChainName}}'根吗?", - "set-root-rulechain-text": "确认之后将变为根规格链并将处理所有接入的传输消息。", - "delete-rulechain-title": " 确实要删除规则链'{{ruleChainName}}'吗?", - "delete-rulechain-text": "请注意:在确认删除后规则链和所有数据将无法恢复。", - "delete-rulechains-title": "确实要删除{count, plural, 1 { 1个规则链} other {#个规则链} }吗?", - "delete-rulechains-action-title": "删除{ count, plural, 1 {1个规则链} other {#个规则链} }", - "delete-rulechains-text": "请注意:确认删除后所有选定的规则链数据将无法恢复。", + "set-root-rulechain-title": "确定要设置'{{ruleChainName}}'为根规则链吗?", + "set-root-rulechain-text": "确认后,规则链将变为根规格链,并将处理所有传入的传输消息。", + "delete-rulechain-title": " 确定要删除规则链'{{ruleChainName}}'吗?", + "delete-rulechain-text": "请注意,确认后,规则链和所有相关数据将不可恢复。", + "delete-rulechains-title": "确定要删除{count, plural, 1 { 1 个规则链} other {# 个规则链} }吗?", + "delete-rulechains-action-title": "删除 { count, plural, 1 {# 个规则链} other {# 个规则链} }", + "delete-rulechains-text": "请注意:确认后,所有选定的规则链将被删除,所有相关的数据将不可恢复。", "add-rulechain-text": "添加新的规则链", "no-rulechains-text": "未找到规则链", "rulechain-details": "规则链详情", @@ -2900,7 +2900,7 @@ "export-failed-error": "无法导出规则链:{{error}}", "create-new-rulechain": "创建新的规则链", "rulechain-file": "规则链文件", - "invalid-rulechain-file-error": "规则链数据结构无效导入失败。", + "invalid-rulechain-file-error": "不能导入规则链:无效的规则链数据格式。", "copyId": "复制规则链ID", "idCopiedMessage": "规则ID已经复制到粘贴板", "select-rulechain": "选择规则链", @@ -2909,27 +2909,27 @@ "management": "规则集管理", "debug-mode": "调试模式", "search": "查找规则链", - "selected-rulechains": "已选择{ count, plural, 1 {1个规则链} other {#个规则链} }", + "selected-rulechains": "已选择 { count, plural, 1 {# 个规则链} other {# 个规则链} }", "open-rulechain": "打开规则链", - "assign-new-rulechain": "分配新规则琏", - "edge-template-root": "根模版", - "assign-to-edge": "分配规则链给Edge", - "edge-rulechain": "Edge规则链", - "unassign-rulechain-from-edge-text": "确认后规则链将取消分配,Edge无法访问。", - "unassign-rulechains-from-edge-title": "你确定要取消分配规则链{ count, plural, 1 {1个规则链} other {#个规则链} }?", - "unassign-rulechains-from-edge-text": "确认后规则链将取消分配,Edge无法访问。", - "assign-rulechain-to-edge-title": "分配规则链给Edge", - "assign-rulechain-to-edge-text": "请选择要分配给Edge的规则链", - "set-edge-template-root-rulechain": "设置规则链为Edge模版根", - "set-edge-template-root-rulechain-title": "你确定将规则链'{{ruleChainName}}'设置为Edge模版根吗?", - "set-edge-template-root-rulechain-text": "确认后规则链将将会成为Edge模版根,且它会成为新创建Edge的根规则链。", + "assign-new-rulechain": "Assign new rulechain", + "edge-template-root": "模版根链", + "assign-to-edge": "分配给边缘", + "edge-rulechain": "边缘规则链", + "unassign-rulechain-from-edge-text": "确认后,规则链将会取消分配,边缘无法访问。", + "unassign-rulechains-from-edge-title": "确定要取消分配 { count, plural, 1 {1 个规则链} other {# 个规则链} }吗?", + "unassign-rulechains-from-edge-text": "确认后,选定的规则链将会取消分配,边缘无法访问。", + "assign-rulechain-to-edge-title": "分配规则链给边缘", + "assign-rulechain-to-edge-text": "请选择要分配给边缘的规则链", + "set-edge-template-root-rulechain": "设置为边缘模版根规则链", + "set-edge-template-root-rulechain-title": "确定将 '{{ruleChainName}}' 设置为边缘模版根规则链吗?", + "set-edge-template-root-rulechain-text": "确认后,将会成为边缘模版根规则链,且它会成为新创建边缘的根规则链。", "invalid-rulechain-type-error": "不能导入规则链:无效的规则链类型。期望类型为{{expectedRuleChainType}}。", - "set-auto-assign-to-edge": "创建时分配规则链给Edge", - "set-auto-assign-to-edge-title": "你确定创建时将规则链'{{ruleChainName}}'自动分配给Edge吗", - "set-auto-assign-to-edge-text": "确认后创建时规则链将自动分配给Edge。", - "unset-auto-assign-to-edge": "创建时分配规则链给Edge", - "unset-auto-assign-to-edge-title": "你确定取消创建时将规则链'{{ruleChainName}}'自动分配给Edge吗?", - "unset-auto-assign-to-edge-text": "确认后创建时规则链将不再自动分配给Edge。", + "set-auto-assign-to-edge": "将规则链分配给新创建的边缘", + "set-auto-assign-to-edge-title": "确定将规则链'{{ruleChainName}}'自动分配给新创建的边缘吗?", + "set-auto-assign-to-edge-text": "确认后,将自动分配规则链给新创建的边缘。", + "unset-auto-assign-to-edge": "不将规则链分配给边缘", + "unset-auto-assign-to-edge-title": "确定不再将规则链'{{ruleChainName}}'自动分配给新创建的边缘吗?", + "unset-auto-assign-to-edge-text": "确认后,将不再自动分配规则链给新创建的边缘。", "unassign-rulechain-title": "你确定要取消分配规则链'{{ruleChainName}}'?", "unassign-rulechains": "取消分配规则链" }, @@ -2941,9 +2941,9 @@ "add": "添加规则节点", "name": "名称", "name-required": "名称必填。", - "name-max-length": "名称长度不能大于256", + "name-max-length": "名称长度必须少于256个字符", "type": "类型", - "description": "描述", + "description": "说明", "delete": "删除规则节点", "select-all-objects": "选择所有节点和连接", "deselect-all-objects": "取消选择所有节点和连接", @@ -2965,15 +2965,15 @@ "custom-link-label-required": "自定义链接标签必填", "link-labels": "链接标签", "link-labels-required": "链接标签必填。", - "no-link-labels-found": "找不到链接标签", - "no-link-label-matching": "找不到'{{label}}'。", + "no-link-labels-found": "未找到链接标签", + "no-link-label-matching": "未找到匹配 '{{label}}' 的链接标签。", "create-new-link-label": "创建一个新的!", - "type-filter": "过滤器", + "type-filter": "筛选器", "type-filter-details": "使用配置条件筛选传入消息", "type-enrichment": "属性集", "type-enrichment-details": "向消息元数据中添加附加信息", "type-transformation": "变换", - "type-transformation-details": "更改消息 ayload和元数据", + "type-transformation-details": "更改消息 Payload 和元数据", "type-action": "动作", "type-action-details": "执行特别动作", "type-external": "外部的", @@ -2986,7 +2986,7 @@ "type-input-details": "规则链的逻辑输入,将传入消息转发到下一个相关规则节点", "type-unknown": "未知", "type-unknown-details": "未解析的规则节点", - "directive-is-not-loaded": "定义的配置指令'{{directiveName}}'不可用。", + "directive-is-not-loaded": "定义的配置指令 '{{directiveName}}' 不可用。", "ui-resources-load-error": "加载配置UI资源失败。", "invalid-target-rulechain": "无法解析目标规则链!", "test-script-function": "测试脚本功能", @@ -3001,7 +3001,7 @@ "output": "输出", "test": "测试", "help": "帮助", - "reset-debug-mode": "恢复所有节点中的调试模式" + "reset-debug-mode": "重置所有节点中的调试模式" }, "timezone": { "timezone": "时区", @@ -3011,44 +3011,44 @@ "browser-time": "浏览器时间" }, "queue": { - "queue-name": "队列名称", - "no-queues-found": "未找到队列。", - "no-queues-matching": "找不到匹配'{{queue}}'的队列。", + "queue-name": "队列", + "no-queues-found": "未找到队列", + "no-queues-matching": "未找到匹配 '{{queue}}' 的队列", "select-name": "选择队列名称", "name": "名称", - "name-required": "队列名称必填!", - "name-unique": "队列名称必须唯一!", - "name-pattern": "队列名称包含ASCII编码的, '.', '_' 和 '-'!", - "queue-required": "队列必填!", - "topic-required": "队列主题必须!", - "poll-interval-required": "轮询间隔必填!", - "poll-interval-min-value": "轮询间隔参数不能小于1", - "partitions-required": "分区必填!", - "partitions-min-value": "分区参数不能小于1", - "pack-processing-timeout-required": "处理超时必填", - "pack-processing-timeout-min-value": "处理超时参数不能小于1", - "batch-size-required": "批量大小必填!", - "batch-size-min-value": "批量大小参数不能小于1", - "retries-required": "重试参数必填!", - "retries-min-value": "重试参数不能为负数", - "failure-percentage-required": "percentage必填!", - "failure-percentage-min-value": "percentage参数不能小于0", - "failure-percentage-max-value": "percentage参数不能大于100", - "pause-between-retries-required": "Pause和retries必填!", - "pause-between-retries-min-value": "Pause和retries参数不能小于1", - "max-pause-between-retries-required": "最大Pause和retries必填!", - "max-pause-between-retries-min-value": "最大Pause和retries参数不能小于1", - "submit-strategy-type-required": "提交策略必填!", - "processing-strategy-type-required": "处理策略必填!", + "name-required": "队列名称必填。", + "name-unique": "队列名称必须唯一。", + "name-pattern": "队列名称不能包含ASCII字母数字以外的字符, '.', '_' 和 '-'等。", + "queue-required": "队列必填。", + "topic-required": "队列主题必填。", + "poll-interval-required": "轮询间隔必填。", + "poll-interval-min-value": "轮询间隔不能小于1", + "partitions-required": "分区必填。", + "partitions-min-value": "分区不能小于1", + "pack-processing-timeout-required": "处理超时时间必填。", + "pack-processing-timeout-min-value": "处理超时时间不能小于1", + "batch-size-required": "批量处理大小必填。", + "batch-size-min-value": "批量处理大小不能小于1", + "retries-required": "重试次数必填。", + "retries-min-value": "重试次数不能为负", + "failure-percentage-required": "失败百分比必填。", + "failure-percentage-min-value": "失败百分比值不能小于0", + "failure-percentage-max-value": "失败百分比值不能大于100", + "pause-between-retries-required": "重试间隔必填。", + "pause-between-retries-min-value": "重试间隔不能小于1", + "max-pause-between-retries-required": "最大重试间隔必填。", + "max-pause-between-retries-min-value": "最大重试间隔不能小于1", + "submit-strategy-type-required": "提交策略类型必填。", + "processing-strategy-type-required": "处理策略类型必填。", "queues": "队列", - "selected-queues": "{ count, plural, 1 {1个队列} other {#个队列} } 选中", - "delete-queue-title": "你确定要删除队列吗'{{queueName}}'?", - "delete-queues-title": "你确定你要删除 { count, plural, 1 {1个队列} other {#个队列} }?", - "delete-queue-text": "确认删除后队列和所有数据将无法恢复。", - "delete-queues-text": "确认删除后所有选定的队列将被将无法访问。", - "search": "查找对列", + "selected-queues": "已选择 { count, plural, 1 {1 个队列} other {# 个队列} }", + "delete-queue-title": "确定要删除 '{{queueName}}' 队列吗?", + "delete-queues-title": "确定要删除 { count, plural, 1 {1 个队列} other {# 个队列} }吗?", + "delete-queue-text": "请注意:确认后,队列和所有相关数据将不可恢复。", + "delete-queues-text": "确认后,所有选定队列都将被删除,无法访问。", + "search": "搜索队列", "add": "添加队列", - "details": "详情", + "details": "队列详情", "topic": "主题", "submit-settings": "提交设置", "submit-strategy": "Strategy type *", @@ -3057,47 +3057,47 @@ "processing-strategy": "Processing type *", "retries-settings": "重试设置", "polling-settings": "轮询设置", - "batch-processing": "分区处理", + "batch-processing": "批量处理", "poll-interval": "轮询间隔", "partitions": "分区", - "immediate-processing": "超时处理", - "consumer-per-partition": "消费者分区遍历", - "consumer-per-partition-hint": "消费者分区单独启用", - "processing-timeout": "处理内部超时", - "batch-size": "批量大小", - "retries": "重试数字(0-无限)", - "failure-percentage": "跳过重试故障消息", - "pause-between-retries": "重试多少秒", - "max-pause-between-retries": "其它重试多少秒", + "immediate-processing": "即时处理", + "consumer-per-partition": "每个分区消费者单独轮询消息", + "consumer-per-partition-hint": "每个分区启用单独的消费者", + "processing-timeout": "处理超时(毫秒)", + "batch-size": "批量处理大小", + "retries": "重试次数 (0 – 无限制)", + "failure-percentage": "跳过重试的失败消息百分比", + "pause-between-retries": "重试间隔(秒)", + "max-pause-between-retries": "最大重试间隔(秒)", "delete": "删除队列", - "copyId": "复制队列Id", - "idCopiedMessage": "队列Id复制到剪贴板", - "description": "描述", - "description-hint": "该文本将显示在队列描述中,而不是选定的策略中。", - "alt-description": "提交策略: {{submitStrategy}},处理策略: {{processingStrategy}}", + "copyId": "复制队列ID", + "idCopiedMessage": "队列ID已复制到剪贴板", + "description": "说明", + "description-hint": "此文本将显示在队列说明中,而不是所选策略中", + "alt-description": "提交策略:{{submitStrategy}},处理策略:{{processingStrategy}}", "strategies": { - "sequential-by-originator-label": "设备顺序", - "sequential-by-originator-hint": "在未确认设备A的消息之前,设备A不会提交新的消息", - "sequential-by-tenant-label": "租户顺序", - "sequential-by-tenant-hint": "在未确认租户A的消息之前,租户A不会提交的新消息", - "sequential-label": "顺序", - "sequential-hint": "在未确认之前的消息,不提交新的消息", - "burst-label": "Burst", - "burst-hint": "所有消息都按照其到达的顺序提交给规则链", - "batch-label": "批量", - "batch-hint": "在未确认之前,不提交新批次", + "sequential-by-originator-label": "按发起者顺序处理", + "sequential-by-originator-hint": "在确认设备A的前一条消息之前,不会提交设备A的新消息", + "sequential-by-tenant-label": "按租户顺序处理", + "sequential-by-tenant-hint": "在确认租户A的前一条消息之前,不会提交租户A的新消息", + "sequential-label": "顺序处理", + "sequential-hint": "在确认前一条消息之前,不会提交新消息", + "burst-label": "突发处理", + "burst-hint": "所有消息都按到达的顺序提交到规则链", + "batch-label": "批量处理", + "batch-hint": "在确认前一批消息之前,不会提交新批", "skip-all-failures-label": "跳过所有失败", "skip-all-failures-hint": "忽略所有失败", "skip-all-failures-and-timeouts-label": "跳过所有失败和超时", "skip-all-failures-and-timeouts-hint": "忽略所有失败和超时", - "retry-all-label": "重试全部", - "retry-all-hint": "重试处理包中所有的消息", - "retry-failed-label": "重试失败", - "retry-failed-hint": "重试处理包中所有失败的消息", - "retry-timeout-label": "重试超时", - "retry-timeout-hint": "重试处理包中所有的定时消息", - "retry-failed-and-timeout-label": "重试失败和超时", - "retry-failed-and-timeout-hint": "重试处理包中所有失败和定时的消息" + "retry-all-label": "全部重试", + "retry-all-hint": "重试处理包中的所有消息", + "retry-failed-label": "失败重试", + "retry-failed-hint": "重试处理包中的所有失败消息", + "retry-timeout-label": "超时重试", + "retry-timeout-hint": "重试处理包中的所有超时消息", + "retry-failed-and-timeout-label": "失败与超时重试", + "retry-failed-and-timeout-hint": "重试处理包中所有失败和超时的消息" } }, "server-error": { @@ -3122,202 +3122,202 @@ "manage-tenant-admins": "管理租户管理员", "delete": "删除租户", "add-tenant-text": "添加新租户", - "no-tenants-text": "没有找到租户", + "no-tenants-text": "未找到租户", "tenant-details": "租客详情", - "title-max-length": "标题长度应小于256", - "delete-tenant-title": "你确定要删除租户'{{tenantTitle}}'吗?", - "delete-tenant-text": "请注意:确认删除后租户数据将无法恢复。", - "delete-tenants-title": "确定要删除{count, plural,1 {1个租户} other {#个租户} }吗?", - "delete-tenants-action-title": "删除{ count, plural, 1 {1个租户} other {#个租户} }", - "delete-tenants-text": "请注意:确认删除后租户数据将无法恢复。", + "title-max-length": "标题长度必须少于256个字符", + "delete-tenant-title": "确定要删除租户'{{tenantTitle}}'吗?", + "delete-tenant-text": "请注意:确认后,租户和所有相关数据将不可恢复。", + "delete-tenants-title": "确定要删除 {count,plural,1 {# 个租户} other {# 个租户} } 吗?", + "delete-tenants-action-title": "删除 { count, plural, 1 {# 个租户} other {# 个租户} }", + "delete-tenants-text": "请注意:确认后,所有选定的租户将被删除,所有相关数据将不可恢复。", "title": "标题", "title-required": "标题必填。", - "description": "描述", + "description": "说明", "details": "详情", "events": "事件", "copyId": "复制租户ID", "idCopiedMessage": "租户ID已经复制到粘贴板", "select-tenant": "选择租户", - "no-tenants-matching": "没有找到符合'{{entity}}'的租户", + "no-tenants-matching": "未找到匹配 '{{entity}}' 的租户", "tenant-required": "租户必填", "search": "查找租户", - "selected-tenants": "已选择{ count, plural, 1 {1个租户} other {#个租户} }", - "isolated-tb-rule-engine": "在独立的ThingsBoard规则引擎容器中处理", - "isolated-tb-rule-engine-details": "每个独立租户需要单独的微服务" + "selected-tenants": "已选择 { count, plural, 1 {# 个租户} other {# 个租户} }", + "isolated-tb-rule-engine": "使用独立的规则引擎服务", + "isolated-tb-rule-engine-details": "每个独立租户需要单独的规则引擎微服务" }, "tenant-profile": { - "tenant-profile": "租户简介", + "tenant-profile": "租户配置", "tenant-profiles": "租户配置", "add": "添加租户配置", "edit": "编辑租户配置", "tenant-profile-details": "租户配置详细信息", - "no-tenant-profiles-text": "找不到租户配置", - "name-max-length": "名称长度不能大于256", + "no-tenant-profiles-text": "未找到租户配置", + "name-max-length": "名称长度必须少于256个字符", "search": "查找租户配置", - "selected-tenant-profiles": "已选择{ count, plural, 1 {1个租户配置} other {#个租户配置} }", - "no-tenant-profiles-matching": "找不到与'{{entity}}'匹配的租户配置。", + "selected-tenant-profiles": "已选择 { count, plural, 1 {# 个租户配置} other {# 个租户配置} }", + "no-tenant-profiles-matching": "未找到与 '{{entity}}' 匹配的租户配置。", "tenant-profile-required": "租户配置必填", - "idCopiedMessage": "租户配置Id已复制到剪贴板", + "idCopiedMessage": "租户配置ID已复制到剪贴板", "set-default": "设置该租户配置为默认", "delete": "删除租户配置", - "copyId": "复制租户配置Id", + "copyId": "复制租户配置ID", "name": "名称", "name-required": "名称必填。", - "data": "纵断面数据", + "data": "配置数据", "profile-configuration": "配置设置", - "description": "描述", + "description": "说明", "default": "默认", - "delete-tenant-profile-title": "确实要删除租户配置'{{tenantProfileName}}'吗?", - "delete-tenant-profile-text": "请注意:确认删除后租户配置和所有相关数据将不可。", - "delete-tenant-profiles-title": "确定要删除{ count, plural, 1 {1个租户配置} other {#个租户配置} }?", - "delete-tenant-profiles-text": "请注意:确认删除后户配置数据将无法恢复。", - "set-default-tenant-profile-title": "确实要将租户配置'{{tenantProfileName}}'设为默认值吗?", - "set-default-tenant-profile-text": "确认后此租户配置将被标记为默认配置,并将用于未指定配置的新租户。", - "no-tenant-profiles-found": "找不到租户配置。", + "delete-tenant-profile-title": "确定要删除租户配置 '{{tenantProfileName}}'吗?", + "delete-tenant-profile-text": "请注意:确认后,租户配置和所有相关数据将不可恢复。", + "delete-tenant-profiles-title": "确定要删除 { count, plural, 1 {# 个租户配置} other {# 个租户配置} }吗?", + "delete-tenant-profiles-text": "请注意:确认后,所有选定的租户配置将被删除,所有相关数据将不可恢复。", + "set-default-tenant-profile-title": "确定要将租户配置 '{{tenantProfileName}}' 设为默认值吗?", + "set-default-tenant-profile-text": "确认后,此租户配置将被标记为默认配置,并将用于未指定配置的新租户。", + "no-tenant-profiles-found": "未找到租户配置。", "create-new-tenant-profile": "创建一个新的!", - "create-tenant-profile": "创建新的租户配置文件", - "import": "导入租户配置文件", - "export": "导出租户配置文件", - "export-failed-error": "导出错误: {{error}}", + "create-tenant-profile": "创建新的租户配置", + "import": "导入租户配置", + "export": "导出租户配置", + "export-failed-error": "无法导出租户配置: {{error}}", "tenant-profile-file": "租户配置文件", - "invalid-tenant-profile-file-error": "导入租户配置文件失败", + "invalid-tenant-profile-file-error": "无法导入租户配置:无效的租户配置数据结构。", "advanced-settings": "高级设置", "entities": "实体", "rule-engine": "规则引擎", - "time-to-live": "过期时间", - "alarms-and-notifications": "警报和通知", - "ota-files-in-bytes": "OTA字节文件", + "time-to-live": "TTL", + "alarms-and-notifications": "告警与通知", + "ota-files-in-bytes": "OTA文件(字节)", "ws-title": "WS", - "unlimited": "(0-无限)", - "maximum-devices": "最大设备数(0-无限制)", + "unlimited": "(0 - 无限制)", + "maximum-devices": "最大设备数", "maximum-devices-required": "最大设备数必填。", - "maximum-devices-range": "最小设备数不能为负数", - "maximum-assets": "最大资产数量(0-无限)", + "maximum-devices-range": "最大设备数不能为负数", + "maximum-assets": "最大资产数", "maximum-assets-required": "最大资产数必填。", "maximum-assets-range": "最大资产数不能为负数", - "maximum-customers": "最大客户数(0-无限制)", + "maximum-customers": "最大客户数", "maximum-customers-required": "最大客户数必填。", "maximum-customers-range": "最大客户数不能为负数", - "maximum-users": "最大用户数(0-无限制)", + "maximum-users": "最大用户数", "maximum-users-required": "最大用户数必填。", "maximum-users-range": "最大用户数不能为负数", - "maximum-dashboards": "仪表板的最大数量(0-无限制)", + "maximum-dashboards": "最大仪表板数", "maximum-dashboards-required": "最大仪表板数必填。", - "maximum-dashboards-range": "仪表板的最大数量不能为负数", - "maximum-rule-chains": "最大规则链数(0-无限制)", + "maximum-dashboards-range": "最大仪表板数不能为负数", + "maximum-rule-chains": "最大规则链数", "maximum-rule-chains-required": "最大规则链数必填。", "maximum-rule-chains-range": "最大规则链数不能为负数", - "maximum-resources-sum-data-size": "资源文件大小总和", - "maximum-resources-sum-data-size-required": "资源文件大小总和必填。", - "maximum-resources-sum-data-size-range": "资源文件总和可能为负", - "maximum-ota-packages-sum-data-size": "OTA包文件大小总和(字节)", - "maximum-ota-package-sum-data-size-required": "OTA包文件大小总和(字节)必填。", - "maximum-ota-package-sum-data-size-range": "OTA包文件大小总和范围", - "transport-tenant-msg-rate-limit": "传输租户消息速率限制", - "transport-tenant-telemetry-msg-rate-limit": "租户遥测消息速率限制", - "transport-tenant-telemetry-data-points-rate-limit": "租户遥测数据点速率限制", - "transport-device-msg-rate-limit": "传输设备消息速率限制", - "transport-device-telemetry-msg-rate-limit": "设备遥测消息速率限制", - "transport-device-telemetry-data-points-rate-limit": "设备遥测数据点速率限制。", + "maximum-resources-sum-data-size": "资源文件总大小", + "maximum-resources-sum-data-size-required": "资源文件总大小必填。", + "maximum-resources-sum-data-size-range": "资源文件总大小不能为负数", + "maximum-ota-packages-sum-data-size": "OTA包文件总大小", + "maximum-ota-package-sum-data-size-required": "OTA包文件总大小必填。", + "maximum-ota-package-sum-data-size-range": "OTA包文件总大小不能为负数", + "transport-tenant-msg-rate-limit": "租户消息", + "transport-tenant-telemetry-msg-rate-limit": "租户遥测消息", + "transport-tenant-telemetry-data-points-rate-limit": "租户遥测数据点", + "transport-device-msg-rate-limit": "设备消息", + "transport-device-telemetry-msg-rate-limit": "设备遥测数据点", + "transport-device-telemetry-data-points-rate-limit": "设备遥测消息", "tenant-entity-export-rate-limit": "实体版本创建", "tenant-entity-import-rate-limit": "实体版本加载", - "max-transport-messages": "最大传输消息数(0-无限制)", + "max-transport-messages": "最大传输消息数", "max-transport-messages-required": "最大传输消息数必填。", - "max-transport-messages-range": "传输消息的最大数目不能为负数", - "max-transport-data-points": "传输数据点的最大数量(0-无限制)", + "max-transport-messages-range": "最大传输消息数不能为负数", + "max-transport-data-points": "最大传输数据点数", "max-transport-data-points-required": "最大传输数据点数必填。", - "max-transport-data-points-range": "传输数据点的最大数目不能为负", - "max-r-e-executions": "最大规则引擎执行数(0-不受限制)", + "max-transport-data-points-range": "最大传输数据点数不能为负", + "max-r-e-executions": "最大规则引擎执行数", "max-r-e-executions-required": "最大规则引擎执行数必填。", - "max-r-e-executions-range": "规则引擎执行的最大数目不能为负", - "max-j-s-executions": "最大JavaScript执行数(0-不受限制)", - "max-j-s-executions-required": "最大JavaScript执行数必填。", - "max-j-s-executions-range": "JavaScript的最大执行数不能为负数", - "max-d-p-storage-days": "最大日存储数据点数(0-无限制)", - "max-d-p-storage-days-required": "最大日存储数据点数必填。", - "max-d-p-storage-days-range": "最大日存储数据点数不能为负数", - "default-storage-ttl-days": "默认存储 TTL天数(0-无限制)", - "default-storage-ttl-days-required": "默认存储TTL天数必填。", - "default-storage-ttl-days-range": "不能为负存储天数", - "alarms-ttl-days": "警报超时(天)", - "alarms-ttl-days-required": "警报超时必填", - "alarms-ttl-days-days-range": "警报超时(天)非负数", - "rpc-ttl-days": "RPC超时(天)", - "rpc-ttl-days-required": "RPC超时(天)必填", - "rpc-ttl-days-days-range": "RPC超时(天)非负数", - "max-rule-node-executions-per-message": "每条消息的最大规则节点执行数(0-无限制)", + "max-r-e-executions-range": "最大规则引擎执行数不能为负", + "max-j-s-executions": "最大 JavaScript 执行数", + "max-j-s-executions-required": "最大 JavaScript 执行数必填。", + "max-j-s-executions-range": "最大 JavaScript 执行数不能为负数", + "max-d-p-storage-days": "最大存储点天", + "max-d-p-storage-days-required": "最大存储点天必填。", + "max-d-p-storage-days-range": "最大存储点天不能为负数", + "default-storage-ttl-days": "默认存储TTL天数", + "default-storage-ttl-days-required": "默认存储TTL天数必填。.", + "default-storage-ttl-days-range": "默认存储TTL天数不能为负数", + "alarms-ttl-days": "告警TTL天数", + "alarms-ttl-days-required": "告警TTL天数必填。", + "alarms-ttl-days-days-range": "告警TTL天数不能为负数", + "rpc-ttl-days": "RPC TTL天数", + "rpc-ttl-days-required": "RPC TTL天数必填。", + "rpc-ttl-days-days-range": "RPC TTL天数不能为负数", + "max-rule-node-executions-per-message": "每条消息的最大规则节点执行数", "max-rule-node-executions-per-message-required": "每个消息的最大规则节点执行数必填。", "max-rule-node-executions-per-message-range": "每条消息的最大规则节点执行数不能为负", - "max-emails": "发送的最大电子邮件数(0-无限制)", + "max-emails": "最大电子邮件发送数", "max-emails-required": "最大电子邮件发送数必填。", - "max-emails-range": "发送的最大电子邮件数不能为负数", - "max-sms": "发送的最大短信数(0-无限制)", + "max-emails-range": "最大电子邮件发送数不能为负", + "max-sms": "最大短信发送数", "max-sms-required": "最大短信发送数必填。", - "max-sms-range": "发送的最大短信数不能为负数", - "max-created-alarms": "警报最大值", - "max-created-alarms-required": "警报最大值必填。", - "max-created-alarms-range": "警报最大值非负数", + "max-sms-range": "最大短信发送数不能为负", + "max-created-alarms": "最大创建告警数", + "max-created-alarms-required": "最大创建告警数必填。", + "max-created-alarms-range": "最大创建告警数不能为负数", "no-queue": "未配置队列", "add-queue": "添加队列", - "queues-with-count": "队列({{count}})", + "queues-with-count": "队列 ({{count}})", "tenant-rest-limits": "租户REST请求", "customer-rest-limits": "客户REST请求", - "incorrect-pattern-for-rate-limits": "此格式以逗号分隔的分区和速率(以秒为单位) 例如:100:1,2000:60", - "too-small-value-zero": "值必须大于0", - "too-small-value-one": "值必须大于1", - "cassandra-tenant-limits-configuration": "租户查询(Cassandra)", - "ws-limit-max-sessions-per-tenant": "租户最大会话数量", - "ws-limit-max-sessions-per-customer": "客户最大会话数量", - "ws-limit-max-sessions-per-regular-user": "用户最大会话数量", - "ws-limit-max-sessions-per-public-user": "公开用户最大会话数量", - "ws-limit-queue-per-session": "消息队列最大会话数量", - "ws-limit-max-subscriptions-per-tenant": "租户订阅最大数量", - "ws-limit-max-subscriptions-per-customer": "客户订阅最大数量", - "ws-limit-max-subscriptions-per-regular-user": "用户订阅最大数量", - "ws-limit-max-subscriptions-per-public-user": "公开用户订阅最大数量", - "ws-limit-updates-per-session": "WS会话更新", + "incorrect-pattern-for-rate-limits": "格式为以冒号分割容量与周期(秒)并以逗号分割配置对,例如 100:1,2000:60", + "too-small-value-zero": "数值必须大于0", + "too-small-value-one": "数值必须大于1", + "cassandra-tenant-limits-configuration": "租户Cassandra查询", + "ws-limit-max-sessions-per-tenant": "租户最大会话数", + "ws-limit-max-sessions-per-customer": "客户最大会话数", + "ws-limit-max-sessions-per-regular-user": "普通用户最大会话数", + "ws-limit-max-sessions-per-public-user": "公共用户最大会话数", + "ws-limit-queue-per-session": "会话最大消息队列大小", + "ws-limit-max-subscriptions-per-tenant": "租户最大订阅数", + "ws-limit-max-subscriptions-per-customer": "客户最大订阅数", + "ws-limit-max-subscriptions-per-regular-user": "普通用户最大订阅数", + "ws-limit-max-subscriptions-per-public-user": "公共用户最大订阅数", + "ws-limit-updates-per-session": "会话WS更新", "rate-limits": { - "add-limit": "Add limit", - "advanced-settings": "Advanced settings", - "edit-limit": "Edit limit", - "but-less-than": "but less than", - "edit-transport-tenant-msg-title": "Edit transport tenant messages rate limits", - "edit-transport-tenant-telemetry-msg-title": "Edit transport tenant telemetry messages rate limits", - "edit-transport-tenant-telemetry-data-points-title": "Edit transport tenant telemetry data points rate limits", - "edit-transport-device-msg-title": "Edit transport device messages rate limits", - "edit-transport-device-telemetry-msg-title": "Edit transport device telemetry messages rate limits", - "edit-transport-device-telemetry-data-points-title": "Edit transport device telemetry data points rate limits", - "edit-transport-tenant-msg-rate-limit-title": "Edit transport tenant messages rate limits", - "edit-customer-rest-limits-title": "Edit REST requests for customer rate limits", - "edit-ws-limit-updates-per-session-title": "Edit WS updates per session rate limits", - "edit-cassandra-tenant-limits-configuration-title": "Edit Cassandra query for tenant rate limits", - "edit-tenant-entity-export-rate-limit-title": "Edit entity version creation rate limits", - "edit-tenant-entity-import-rate-limit-title": "Edit entity version load rate limits", - "messages-per": "messages per", - "not-set": "Not set", - "number-of-messages": "Number of messages", - "number-of-messages-required": "Number of messages is required.", - "number-of-messages-min": "Minimum value is 1.", - "preview": "Preview", - "per-seconds": "Per seconds", - "per-seconds-required": "Time rate is required.", - "per-seconds-min": "Minimum value is 1.", - "rate-limits": "Rate limits", - "remove-limit": "Remove limit", - "transport-tenant-msg": "Transport tenant messages", - "transport-tenant-telemetry-msg": "Transport tenant telemetry messages", - "transport-tenant-telemetry-data-points": "Transport tenant telemetry data points", - "transport-device-msg": "Transport device messages", - "transport-device-telemetry-msg": "Transport device telemetry messages", - "transport-device-telemetry-data-points": "Transport device telemetry data points", - "sec": "sec" + "add-limit": "添加限制", + "advanced-settings": "高级设置", + "edit-limit": "编辑限制", + "but-less-than": "但小于", + "edit-transport-tenant-msg-title": "编辑传输租户消息速率限制", + "edit-transport-tenant-telemetry-msg-title": "编辑传输租户遥测消息速率限制", + "edit-transport-tenant-telemetry-data-points-title": "编辑传输租户遥测数据点速率限制", + "edit-transport-device-msg-title": "编辑传输设备消息速率限制", + "edit-transport-device-telemetry-msg-title": "编辑传输设备遥测消息速率限制", + "edit-transport-device-telemetry-data-points-title": "编辑传输设备遥测数据点速率限制", + "edit-transport-tenant-msg-rate-limit-title": "编辑传输租户消息速率限制", + "edit-customer-rest-limits-title": "编辑客户REST请求速率限制", + "edit-ws-limit-updates-per-session-title": "编辑会话WS更新速率限制", + "edit-cassandra-tenant-limits-configuration-title": "编辑租户Cassandra查询速率限制", + "edit-tenant-entity-export-rate-limit-title": "编辑实体版本创建速率限制", + "edit-tenant-entity-import-rate-limit-title": "编辑实体版本加载速率限制", + "messages-per": "条消息每", + "not-set": "未配置", + "number-of-messages": "消息数量", + "number-of-messages-required": "消息数量必填。", + "number-of-messages-min": "最小值为1。", + "preview": "预览", + "per-seconds": "每秒", + "per-seconds-required": "时间比率必填。", + "per-seconds-min": "最小值为1。", + "rate-limits": "速率限制", + "remove-limit": "删除限制", + "transport-tenant-msg": "传输租户消息", + "transport-tenant-telemetry-msg": "传输租户遥测消息", + "transport-tenant-telemetry-data-points": "传输租户遥测数据点", + "transport-device-msg": "传输设备消息", + "transport-device-telemetry-msg": "传输设备遥测消息", + "transport-device-telemetry-data-points": "传输设备遥测数据点", + "sec": "秒" } }, "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {#秒} other {#秒} }", - "minutes-interval": "{ minutes, plural, 1 {#分} other {#分} }", - "hours-interval": "{ hours, plural, 1 {#小时} other {#小时} }", - "days-interval": "{ days, plural, 1 {#天} other {#天} }", + "seconds-interval": "{ seconds, plural, 1 {# 秒} other {# 秒} }", + "minutes-interval": "{ minutes, plural, 1 {# 分} other {# 分} }", + "hours-interval": "{ hours, plural, 1 {# 小时} other {# 小时} }", + "days-interval": "{ days, plural, 1 {# 天} other {# 天} }", "days": "天", "hours": "小时", "minutes": "分钟", @@ -3326,22 +3326,22 @@ "predefined": { "yesterday": "昨天", "day-before-yesterday": "前天", - "this-day-last-week": "上周今天", - "previous-week": "上周(周日至周六)", - "previous-week-iso": "上周(周一至周日)", - "previous-month": "上月", - "previous-year": "去年", - "current-hour": "当时", - "current-day": "今天", - "current-day-so-far": "今天当前", - "current-week": "本周(周日至周六)", - "current-week-iso": "本周(周一至周日)", - "current-week-so-far": "本周(周至周六)", - "current-week-iso-so-far": "本周(周一至周日)", + "this-day-last-week": "前一周的这一天", + "previous-week": "前一周(周日至周六)", + "previous-week-iso": "前一周(周一至周日)", + "previous-month": "前一个月", + "previous-year": "前一年", + "current-hour": "当前小时", + "current-day": "当前天", + "current-day-so-far": "当天到目前为止", + "current-week": "本周(周日至周六)", + "current-week-iso": "本周(周一至周日)", + "current-week-so-far": "本周到目前为止(周日至周六)", + "current-week-iso-so-far": "本周到目前为止(周一至周日)", "current-month": "本月", - "current-month-so-far": "本月当前", - "current-year": "今年", - "current-year-so-far": "今年当前" + "current-month-so-far": "本月到目前为止", + "current-year": "本年", + "current-year-so-far": "本年到目前为止" } }, "timeunit": { @@ -3352,20 +3352,20 @@ "days": "天" }, "timewindow": { - "days": "{ days, plural, 1 {#天 } other {#天 } }", - "hours": "{ hours, plural, 0 {-小时 } 1 {#小时 } other {#小时 } }", - "minutes": "{ minutes, plural, 0 {-分 } 1 {#分 } other {#分 } }", - "seconds": "{ seconds, plural, 0 {-秒 } 1 {#秒 } other {#秒 } }", + "days": "{ days, plural, 1 {# 天 } other {# 天 } }", + "hours": "{ hours, plural, 0 {- 小时 } 1 {# 小时 } other {# 小时 } }", + "minutes": "{ minutes, plural, 0 {- 分 } 1 {# 分 } other {# 分 } }", + "seconds": "{ seconds, plural, 0 {- 秒 } 1 {# 秒 } other {# 秒 } }", "realtime": "实时", "history": "历史", "last-prefix": "最后", - "period": "从{{ startTime }}到{{ endTime }}", + "period": "从 {{ startTime }} 到 {{ endTime }}", "edit": "编辑时间窗口", "date-range": "日期范围", "last": "最后", - "time-period": "时间范围", + "time-period": "时间段", "hide": "隐藏", - "interval": "时间间隔" + "interval": "区间" }, "user": { "user": "用户", @@ -3379,38 +3379,38 @@ "add": "添加用户", "delete": "删除用户", "add-user-text": "添加新用户", - "no-users-text": "找不到用户", + "no-users-text": "未找到用户", "user-details": "用户详细信息", - "delete-user-title": "你确定要删除用户'{{userEmail}}'吗?", - "delete-user-text": "请注意:确认删除后用户数据将无法恢复。", - "delete-users-title": "确定要删除 { count, plural, 1 {1个用户} other {#个用户} } 吗?", - "delete-users-action-title": "删除 { count, plural, 1 {1个用户} other {#个用户} }", - "delete-users-text": "请注意:确认删除后用户数据将无法恢复。", + "delete-user-title": "确定要删除用户 '{{userEmail}}' 吗?", + "delete-user-text": "请注意:确认后,用户和所有相关数据将不可恢复。", + "delete-users-title": "确定要删除 { count, plural, 1 {# 个用户} other {# 个用户} } 吗?", + "delete-users-action-title": "删除 { count, plural, 1 {# 个用户} other {# 个用户} }", + "delete-users-text": "请注意:确认后,所有选定的用户将被删除,所有相关数据将不可恢复。", "activation-email-sent-message": "激活电子邮件已成功发送!", "resend-activation": "重新发送激活", "email": "电子邮件", "email-required": "电子邮件必填。", "invalid-email-format": "无效的邮件格式。", "first-name": "名字", - "last-name": "姓", - "description": "描述", + "last-name": "姓氏", + "description": "说明", "default-dashboard": "默认面板", "always-fullscreen": "始终全屏", "select-user": "选择用户", - "no-users-matching": "没有找到符合'{{entity}}'的用户。", + "no-users-matching": "未找到匹配 '{{entity}}' 的用户。", "user-required": "用户必填", "activation-method": "激活方式", "display-activation-link": "显示激活链接", "send-activation-mail": "发送激活邮件", "activation-link": "用户激活链接", - "activation-link-text": "使用该链接激活 激活用户:", + "activation-link-text": "使用该链接 激活 激活用户:", "copy-activation-link": "复制用户激活链接", "activation-link-copied-message": "用户激活链接已经复制到粘贴板", "details": "详情", "login-as-tenant-admin": "以租户管理员身份登录", "login-as-customer-user": "以客户用户身份登录", "search": "查找用户", - "selected-users": "已选择{count, plural, 1 {1个用户} other {#个用户} }", + "selected-users": "已选择 { count, plural, 1 {# 个用户} other {# 个用户} }", "disable-account": "禁用用户帐户", "enable-account": "启用用户帐户", "enable-account-message": "已成功启用用户帐户!", @@ -3446,72 +3446,72 @@ "search": "搜索版本", "branch": "分支", "default": "默认", - "select-branch": "选择版本", + "select-branch": "选择分支", "branch-required": "分支必填", "create-entity-version": "创建实体版本", "version-name": "版本名称", "version-name-required": "版本名称必填", "author": "作者", - "export-relations": "导出关系", + "export-relations": "导出关联", "export-attributes": "导出属性", - "export-credentials": "导出任证", + "export-credentials": "导出凭据", "entity-versions": "实体版本", "versions": "版本", "created-time": "创建时间", "version-id": "版本ID", - "no-entity-versions-text": "没有找到实体版本", - "no-versions-text": "没有找到版本", - "copy-full-version-id": "复制全部版本ID", + "no-entity-versions-text": "未找到实体版本", + "no-versions-text": "未找到版本", + "copy-full-version-id": "复制完整版本ID", "create-version": "创建版本", - "creating-version": "正在创建版本请等待. . .", - "nothing-to-commit": "没有更改的变化", + "creating-version": "请稍候,正在创建版本...", + "nothing-to-commit": "没有要提交的更改", "restore-version": "还原版本", - "restore-entity-from-version": "从版'{{versionName}}恢复实体'", - "restoring-entity-version": "正在恢复版本请等待. . .", - "load-relations": "加载关系", + "restore-entity-from-version": "从版本 '{{versionName}}' 还原实体", + "restoring-entity-version": "请稍候,正在还原实体版本...", + "load-relations": "加载关联", "load-attributes": "加载属性", - "load-credentials": "加载凭证", - "compare-with-current": "与当前进行比较", - "diff-entity-with-version": "与实体版本'{{versionName}}'", - "previous-difference": "以前差异", + "load-credentials": "加载凭据", + "compare-with-current": "与当前比较", + "diff-entity-with-version": "与实体版本 '{{versionName}}' 不同", + "previous-difference": "上一个差异", "next-difference": "下一个差异", "current": "当前", - "differences": "{ count, plural, 1 {1处差异} other {#处差异} }", + "differences": "{ count, plural, 1 {1 个差异} other {# 个差异} }", "create-entities-version": "创建实体版本", "default-sync-strategy": "默认同步策略", "sync-strategy-merge": "合并", "sync-strategy-overwrite": "覆盖", - "entities-to-export": "实体导出", - "entities-to-restore": "实体恢复", + "entities-to-export": "导出的实体", + "entities-to-restore": "还原的实体", "sync-strategy": "同步策略", "all-entities": "所有实体", - "no-entities-to-export-prompt": "请指定导出的实体", - "no-entities-to-restore-prompt": "请指定要恢复的实体", + "no-entities-to-export-prompt": "请指定要导出的实体", + "no-entities-to-restore-prompt": "请指定要还原的实体", "add-entity-type": "添加实体类型", - "remove-all": "移除全部", - "version-create-result": "{ added, plural, 0 {没有实体} 1 {1实体} other {#实体} }添加
{ modified, plural, 0 {没有实体} 1 {1实体} other {#实体} }修改。
{ removed, plural, 0 {没有实体} 1 {1实体} other {#实体} }移除。", - "remove-other-entities": "移除其它实体", - "find-existing-entity-by-name": "根据名称查找存在实体", - "restore-entities-from-version": "从版本'{{versionName}}'恢复实体", - "restoring-entities-from-version": "正在从版本恢复实体请等待. . .", - "no-entities-restored": "没有恢复实体", - "created": "{{created}}创建", - "updated": "{{updated}}更新", - "deleted": "{{deleted}}删除", - "remove-other-entities-confirm-text": "请注意:将永久删除你要恢复的版本中不存在所有当前实体
请输入要移除其它实体进行确认。", - "auto-commit-to-branch": "自动提交{{ branch }}分支", - "default-create-entity-version-name": "{{entityName}}更新", - "sync-strategy-merge-hint": "在仓库中创建或更新选中实体,其他仓库实体不会被修改。", - "sync-strategy-overwrite-hint": "在仓库中创建或更新选中实体,其他仓库实体同步被删除。", - "device-credentials-conflict": "无法载外部id为{{entityId}}的设备
因为已经有相同的数据存在数据库中。
请考虑禁用还原窗口中的加载凭据设置。", - "missing-referenced-entity": "未能加载外部id为{{sourceEntityTypeName}}和{{sourceEntityId}}
因为引用缺少{{targetEntityTypeName}}和b>{{targetEntityId}}。", - "runtime-failed": "失败:{{message}}", + "remove-all": "全部删除", + "version-create-result": "{ added, plural, 0 {没有实体} 1 {1 个实体} other {# 个实体} } 被添加。
{ modified, plural, 0 {没有实体} 1 {1 个实体} other {# 个实体} } 被修改。
{ removed, plural, 0 {没有实体} 1 {1 个实体} other {# 个实体} } 被删除。", + "remove-other-entities": "删除其他实体", + "find-existing-entity-by-name": "按名称查找现有实体", + "restore-entities-from-version": "从版本 '{{versionName}}' 还原实体", + "restoring-entities-from-version": "请稍候,正在还原实体...", + "no-entities-restored": "未还原任何实体", + "created": "{{created}} 创建", + "updated": "{{updated}} 更新", + "deleted": "{{deleted}} 删除", + "remove-other-entities-confirm-text": "请注意!在还原版本中不存在的当前实体
将被永久 删除。

请输入 remove other entities 进行确认。", + "auto-commit-to-branch": "自动提交到 {{ branch }} 分支", + "default-create-entity-version-name": "{{entityName}} 更新", + "sync-strategy-merge-hint": "创建或更新选定的实体,仓库其他实体均不修改。", + "sync-strategy-overwrite-hint": "创建或更新选定的实体,仓库其他实体将被删除。", + "device-credentials-conflict": "无法加载外部ID为 {{entityId}} 的设备
因为数据库中已存在相同的凭据。
请考虑禁用还原表单中的 加载凭据 设置。", + "missing-referenced-entity": "无法加载外部ID为 {{sourceEntityId}} 的 {{sourceEntityTypeName}}
因为它引用了缺失的 {{targetEntityTypeName}} (ID:{{targetEntityId}}).", + "runtime-failed": "失败: {{message}}", "auto-commit-settings-read-only-hint": "自动提交功能不适用于存储库设置中启用的只读选项。" }, "widget": { - "widget-library": "部件包", + "widget-library": "部件库", "widget-bundle": "部件包", - "all-bundles": "所有部件包", + "all-bundles": "All bundles", "select-widgets-bundle": "选择部件包", "management": "管理部件", "editor": "部件编辑器", @@ -3519,19 +3519,19 @@ "widget-type-load-error": "由于以下错误未加载小部件:", "remove": "删除部件", "edit": "编辑部件", - "remove-widget-title": "确实要删除'{{widgetTitle}}'部件吗?", - "remove-widget-text": "确认删除后数据将无法恢复。", - "timeseries": "时间序列", + "remove-widget-title": "确定要删除 '{{widgetTitle}}'部件吗?", + "remove-widget-text": "确认后,控件和所有相关数据将变得不可恢复。", + "timeseries": "Timeseries", "search-data": "查找数据", - "no-data-found": "没有找到数据", + "no-data-found": "未找到数据", "latest": "最新值", "rpc": "控件部件", - "alarm": "警报部件", + "alarm": "告警部件", "static": "静态部件", "select-widget-type": "选择窗口部件类型", "missing-widget-title-error": "部件标题必须指定!", "widget-saved": "部件已保存", - "unable-to-save-widget-error": "数据错误部件无法保存!", + "unable-to-save-widget-error": "无法保存部件!控件有错误!", "save": "保存部件", "saveAs": "部件另存为", "save-widget-type-as": "部件类型另存为", @@ -3547,7 +3547,7 @@ "remove-resource": "删除资源", "add-resource": "添加资源", "html": "HTML", - "tidy": "对齐", + "tidy": "Tidy", "css": "CSS", "settings-schema": "设置模式", "datakey-settings-schema": "数据键设置模式", @@ -3560,8 +3560,8 @@ "latest-data-key-settings-form-selector": "最新数据键设置表单选择器", "javascript": "Javascript", "js": "JS", - "remove-widget-type-title": "你确定要删除部件类型'{{widgetName}}'吗?", - "remove-widget-type-text": "确认删除后部件类型数据将无法恢复。", + "remove-widget-type-title": "确定要删除部件类型 '{{widgetName}}'吗?", + "remove-widget-type-text": "确认后,窗口部件类型和所有相关数据将不可恢复。", "remove-widget-type": "删除部件类型", "add-widget-type": "添加新的部件类型", "widget-type-load-failed-error": "无法加载部件类型!", @@ -3637,20 +3637,20 @@ "delete": "删除部件包", "title": "标题", "title-required": "标题必填。", - "title-max-length": "标题长度应小于256", + "title-max-length": "标题长度必须少于256个字符", "description": "描述", "image-preview": "图片预览", "add-widgets-bundle-text": "添加新的部件包", - "no-widgets-bundles-text": "找不到部件包", + "no-widgets-bundles-text": "未找到部件包", "empty": "部件包是空的", "details": "详情", "widgets-bundle-details": "部件包详细信息", - "delete-widgets-bundle-title": "你确定要删除部件包'{{widgetsBundleTitle}}'吗?", - "delete-widgets-bundle-text": "请注意:确认删除后部件包数据将无法恢复。", - "delete-widgets-bundles-title": "确定要删除{ count, plural, 1 {1个部件包} other {#个部件包} }吗?", - "delete-widgets-bundles-action-title": "删除{ count, plural, 1 {1个部件包} other {#个部件包} }", - "delete-widgets-bundles-text": "请注意:确认删除后部件包数据将无法恢复。", - "no-widgets-bundles-matching": "没有找到与'{{widgetsBundle}}'匹配的部件包。", + "delete-widgets-bundle-title": "确定要删除部件包 '{{widgetsBundleTitle}}'吗?", + "delete-widgets-bundle-text": "请注意:确认后,部件包和所有相关数据将不可恢复。", + "delete-widgets-bundles-title": "确定要删除 { count, plural, 1 {# 个部件包} other {# 个部件包} } 吗?", + "delete-widgets-bundles-action-title": "删除 { count, plural, 1 {# 个部件包} other {# 个部件包} }", + "delete-widgets-bundles-text": "请注意:确认后,所有选定的部件包将被删除,所有相关数据将不可恢复。", + "no-widgets-bundles-matching": "未找到与 '{{widgetsBundle}}' 匹配的部件包。", "widgets-bundle-required": "部件包必填。", "system": "系统", "import": "导入部件包", @@ -3658,9 +3658,9 @@ "export-failed-error": "无法导出部件包: {{error}}", "create-new-widgets-bundle": "创建新的部件包", "widgets-bundle-file": "部件包文件", - "invalid-widgets-bundle-file-error": "部件包数据结构错误无法导入。", + "invalid-widgets-bundle-file-error": "无法导入部件包:无效的部件包数据结构。", "search": "查找部件包", - "selected-widgets-bundles": "已选择{ count, plural, 1 {1个部件包} other {#个部件包} }", + "selected-widgets-bundles": "已选择 { count, plural, 1 {# 个部件包} other {# 个部件包} }", "open-widgets-bundle": "打开部件包", "loading-widgets-bundles": "加载部件包..." }, @@ -3735,7 +3735,7 @@ "export-failed-error": "无法导出部件类型: {{error}}", "create-new-widget-type": "创建新的部件类型", "widget-type-file": "部件类型文件", - "invalid-widget-type-file-error": "部件类型数据结构错误无法导入。" + "invalid-widget-type-file-error": "无法导入部件类型:无效的部件类型数据结构。" }, "widgets": { "chart": { @@ -4729,11 +4729,11 @@ "show-all": "显示所有图标" }, "phone-input": { - "phone-input-label": "电话号码", - "phone-input-required": "电话号码必填。", - "phone-input-validation": "电话号码无效或不可用。", - "phone-input-pattern": "电话号码无效应该是E.164格式例如:{{phoneNumber}}", - "phone-input-hint": "电话号码无效应该是E.164格式例如:{{phoneNumber}}" + "phone-input-label": "手机号码", + "phone-input-required": "手机号码必填", + "phone-input-validation": "手机号码无效或不存在", + "phone-input-pattern": "无效的手机号码。应为E.164格式,例如:{{phoneNumber}}", + "phone-input-hint": "E.164格式手机号码,例如:{{phoneNumber}}" }, "custom": { "widget-action": { From 7e8f7a02ca0196ec79e514ff91e136f0c6456d2a Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 9 Nov 2022 12:13:19 +0200 Subject: [PATCH 073/226] Refactoring code --- application/src/main/data/upgrade/3.4.1/schema_update.sql | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/application/src/main/data/upgrade/3.4.1/schema_update.sql b/application/src/main/data/upgrade/3.4.1/schema_update.sql index 0f4354c0ca..8dceb0115d 100644 --- a/application/src/main/data/upgrade/3.4.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.1/schema_update.sql @@ -14,6 +14,7 @@ -- limitations under the License. -- +-- AUDIT LOGS MIGRATION START DO $$ DECLARE table_partition RECORD; @@ -73,8 +74,10 @@ BEGIN WHERE created_time >= start_time_ms AND created_time < end_time_ms; END; $$; +-- AUDIT LOGS MIGRATION END +-- EDGE EVENTS MIGRATION START DO $$ DECLARE table_partition RECORD; @@ -131,3 +134,4 @@ BEGIN WHERE created_time >= start_time_ms AND created_time < end_time_ms; END; $$; +-- EDGE EVENTS MIGRATION END From d1312cb917c15eddfc685a8502a67ffcadda3db0 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 9 Nov 2022 12:54:17 +0200 Subject: [PATCH 074/226] Added serviceId and sessionId to deviceRpcCallmsg --- .../rpc/constructor/DeviceMsgConstructor.java | 45 ++++++++++++------- .../rpc/processor/DeviceEdgeProcessor.java | 11 +++-- common/edge-api/src/main/proto/edge.proto | 2 + .../rule/engine/rpc/TbSendRPCReplyNode.java | 20 ++++++--- 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java index 49438792fa..ebea5d482f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java @@ -125,23 +125,36 @@ public class DeviceMsgConstructor { } private DeviceRpcCallMsg.Builder constructDeviceRpcMsg(UUID deviceId, JsonNode body) { - int requestId = body.get("requestId").asInt(); - boolean oneway = body.get("oneway").asBoolean(); - UUID requestUUID = UUID.fromString(body.get("requestUUID").asText()); - long expirationTime = body.get("expirationTime").asLong(); - boolean persisted = body.get("persisted").asBoolean(); - int retries = body.get("retries").asInt(); - String additionalInfo = body.get("additionalInfo").asText(); - return DeviceRpcCallMsg.newBuilder() + DeviceRpcCallMsg.Builder builder = DeviceRpcCallMsg.newBuilder() .setDeviceIdMSB(deviceId.getMostSignificantBits()) .setDeviceIdLSB(deviceId.getLeastSignificantBits()) - .setRequestUuidMSB(requestUUID.getMostSignificantBits()) - .setRequestUuidLSB(requestUUID.getLeastSignificantBits()) - .setExpirationTime(expirationTime) - .setRequestId(requestId) - .setOneway(oneway) - .setPersisted(persisted) - .setRetries(retries) - .setAdditionalInfo(additionalInfo); + .setRequestId(body.get("requestId").asInt()); + if (body.get("oneway") != null) { + builder.setOneway(body.get("oneway").asBoolean()); + } + if (body.get("requestUUID") != null) { + UUID requestUUID = UUID.fromString(body.get("requestUUID").asText()); + builder.setRequestUuidMSB(requestUUID.getMostSignificantBits()) + .setRequestUuidLSB(requestUUID.getLeastSignificantBits()); + } + if (body.get("expirationTime") != null) { + builder.setExpirationTime(body.get("expirationTime").asLong()); + } + if (body.get("persisted") != null) { + builder.setPersisted(body.get("persisted").asBoolean()); + } + if (body.get("retries") != null) { + builder.setRetries(body.get("retries").asInt()); + } + if (body.get("additionalInfo") != null) { + builder.setAdditionalInfo(JacksonUtil.toString(body.get("additionalInfo"))); + } + if (body.get("serviceId") != null) { + builder.setServiceId(body.get("serviceId").asText()); + } + if (body.get("sessionId") != null) { + builder.setSessionId(body.get("sessionId").asText()); + } + return builder; } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java index b340b252ef..959f5c5fe8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java @@ -376,7 +376,8 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { String requestId = Integer.toString(deviceRpcCallMsg.getRequestId()); metaData.putValue("requestId", requestId); metaData.putValue("requestUUID", requestUUID.toString()); - // ?? metaData.putValue("originServiceId", deviceRpcRequestMsg.get); + metaData.putValue("serviceId", deviceRpcCallMsg.getServiceId()); + metaData.putValue("sessionId", deviceRpcCallMsg.getSessionId()); metaData.putValue("expirationTime", Long.toString(deviceRpcCallMsg.getExpirationTime())); metaData.putValue("oneway", Boolean.toString(deviceRpcCallMsg.getOneway())); metaData.putValue(DataConstants.PERSISTENT, Boolean.toString(deviceRpcCallMsg.getPersisted())); @@ -403,16 +404,18 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { - log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", device); + log.debug("Successfully send TO_SERVER_RPC_REQUEST to rule engine [{}], deviceRpcCallMsg {}", + device, deviceRpcCallMsg); } @Override public void onFailure(Throwable t) { - log.debug("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", device, t); + log.debug("Failed to send TO_SERVER_RPC_REQUEST to rule engine [{}], deviceRpcCallMsg {}", + device, deviceRpcCallMsg, t); } }); } catch (JsonProcessingException | IllegalArgumentException e) { - log.warn("[{}] Failed to push device action to rule engine: {}", deviceId, DataConstants.ENTITY_CREATED, e); + log.warn("[{}] Failed to push TO_SERVER_RPC_REQUEST to rule engine. deviceRpcCallMsg {}", deviceId, deviceRpcCallMsg, e); } return Futures.immediateFuture(null); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index a5785d6870..d70179cfe0 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -433,6 +433,8 @@ message DeviceRpcCallMsg { bool persisted = 10; int32 retries = 11; string additionalInfo = 12; + string serviceId = 13; + string sessionId = 14; } message RpcRequestMsg { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java index 22d13f196b..89bc55bb54 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java @@ -15,6 +15,8 @@ */ package org.thingsboard.rule.engine.rpc; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -78,7 +80,11 @@ public class TbSendRPCReplyNode implements TbNode { ctx.tellFailure(msg, new RuntimeException("Request body is empty!")); } else { if (StringUtils.isNotBlank(msg.getMetaData().getValue(DataConstants.EDGE_ID))) { - saveRpcResponseToEdgeQueue(ctx, msg); + try { + saveRpcResponseToEdgeQueue(ctx, msg, serviceIdStr, sessionIdStr, requestIdStr); + } catch (Exception e) { + ctx.tellFailure(msg, e); + } } else { ctx.getRpcService().sendRpcReplyToDevice(serviceIdStr, UUID.fromString(sessionIdStr), Integer.parseInt(requestIdStr), msg.getData()); ctx.tellSuccess(msg); @@ -86,7 +92,7 @@ public class TbSendRPCReplyNode implements TbNode { } } - private void saveRpcResponseToEdgeQueue(TbContext ctx, TbMsg msg) { + private void saveRpcResponseToEdgeQueue(TbContext ctx, TbMsg msg, String serviceIdStr, String sessionIdStr, String requestIdStr) throws JsonProcessingException { // EdgeEvent edgeEvent = new EdgeEvent(); // edgeEvent.setTenantId(tenantId); // edgeEvent.setAction(eventAction); @@ -95,8 +101,11 @@ public class TbSendRPCReplyNode implements TbNode { // edgeEvent.setBody(entityBody); // edgeEvent.setEdgeId(edgeId); // -// ObjectNode body = mapper.createObjectNode(); -// body.put("requestId", requestId); + ObjectNode body = JacksonUtil.OBJECT_MAPPER.createObjectNode(); + body.put("serviceId", serviceIdStr); + body.put("sessionId", sessionIdStr); + body.put("requestId", requestIdStr); + body.put("response", JacksonUtil.OBJECT_MAPPER.writeValueAsString(msg.getData())); // body.put("requestUUID", msg.getId().toString()); // body.put("oneway", msg.isOneway()); // body.put("expirationTime", msg.getExpirationTime()); @@ -111,7 +120,7 @@ public class TbSendRPCReplyNode implements TbNode { // TODO: add body EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(ctx.getTenantId(), edgeId, EdgeEventType.DEVICE, - EdgeEventActionType.RPC_CALL_RESPONSE, deviceId, JacksonUtil.OBJECT_MAPPER.valueToTree("{}")); + EdgeEventActionType.RPC_CALL_RESPONSE, deviceId, JacksonUtil.OBJECT_MAPPER.valueToTree(body)); ListenableFuture future = ctx.getEdgeEventService().saveAsync(edgeEvent); Futures.addCallback(future, new FutureCallback() { @@ -123,6 +132,7 @@ public class TbSendRPCReplyNode implements TbNode { @Override public void onFailure(Throwable t) { + ctx.tellFailure(msg, t); } }, ctx.getDbCallbackExecutor()); } From d9774ccfda1a58256dbab72bd06203be0d012e08 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 7 Sep 2022 16:44:05 +0300 Subject: [PATCH 075/226] http client returns headers as array is there is more than one, put headers to metadata for failure response --- .../rule/engine/rest/TbHttpClient.java | 22 ++++++++++++++++++- .../rule/engine/rest/TbHttpClientTest.java | 22 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 58bb5c7564..b7414c5e0d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -41,6 +41,7 @@ import org.springframework.util.concurrent.ListenableFutureCallback; import org.springframework.web.client.AsyncRestTemplate; import org.springframework.web.client.RestClientResponseException; import org.springframework.web.util.UriComponentsBuilder; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbRelationTypes; @@ -59,8 +60,11 @@ import java.net.URI; import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.util.Deque; +import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; @Data @Slf4j @@ -244,17 +248,33 @@ public class TbHttpClient { metaData.putValue(STATUS, response.getStatusCode().name()); metaData.putValue(STATUS_CODE, response.getStatusCode().value() + ""); metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase()); - response.getHeaders().toSingleValueMap().forEach(metaData::putValue); + headersToMetaData(response.getHeaders(), metaData::putValue); String body = response.getBody() == null ? "{}" : response.getBody(); return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, body); } + void headersToMetaData(Map> headers, BiConsumer consumer) { + if (headers == null) { + return; + } + headers.forEach((key, values) -> { + if (values != null && !values.isEmpty()) { + if (values.size() == 1) { + consumer.accept(key, values.get(0)); + } else { + consumer.accept(key, JacksonUtil.toString(values)); + } + } + }); + } + private TbMsg processFailureResponse(TbContext ctx, TbMsg origMsg, ResponseEntity response) { TbMsgMetaData metaData = origMsg.getMetaData(); metaData.putValue(STATUS, response.getStatusCode().name()); metaData.putValue(STATUS_CODE, response.getStatusCode().value() + ""); metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase()); metaData.putValue(ERROR_BODY, response.getBody()); + headersToMetaData(response.getHeaders(), metaData::putValue); return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java index f7a1d83ac9..28918538e2 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.rest; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; +import org.assertj.core.api.Assertions; import org.awaitility.Awaitility; import org.junit.After; import org.junit.Assert; @@ -26,6 +27,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockserver.integration.ClientAndServer; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.AsyncRestTemplate; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.id.DeviceId; @@ -34,6 +36,8 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import java.net.URI; +import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -200,5 +204,23 @@ public class TbHttpClientTest { ); } + @Test + public void testHeadersToMetaData() { + Map> headers = new LinkedMultiValueMap<>(); + headers.put("Content-Type", List.of("binary")); + headers.put("Set-Cookie", List.of("sap-context=sap-client=075; path=/", "sap-token=sap-client=075; path=/")); + + TbMsgMetaData metaData = new TbMsgMetaData(); + + willCallRealMethod().given(client).headersToMetaData(any(), any()); + + client.headersToMetaData(headers, metaData::putValue); + + Map data = metaData.getData(); + + Assertions.assertThat(data).hasSize(2); + Assertions.assertThat(data.get("Content-Type")).isEqualTo("binary"); + Assertions.assertThat(data.get("Set-Cookie")).isEqualTo("[\"sap-context=sap-client=075; path=/\",\"sap-token=sap-client=075; path=/\"]"); + } } \ No newline at end of file From 3cb94532fabd40fa67a1394234daad74c48a90f5 Mon Sep 17 00:00:00 2001 From: dlandiak Date: Wed, 9 Nov 2022 14:03:37 +0200 Subject: [PATCH 076/226] get inactivity using entity data query instead of separate attribute fetch request --- .../state/DefaultDeviceStateService.java | 20 ++++------- .../state/DefaultDeviceStateServiceTest.java | 36 +++++++++++-------- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index 38f4ee50c7..4aaba9c6ea 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -84,7 +84,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.UUID; @@ -123,7 +122,8 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService PERSISTENT_ATTRIBUTE_KEYS = Arrays.asList( new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, LAST_ACTIVITY_TIME), @@ -662,24 +662,16 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService attributeOpt = attributesService.find(TenantId.SYS_TENANT_ID, deviceStateData.getDeviceId(), SERVER_SCOPE, INACTIVITY_TIMEOUT).get(); - attributeOpt.flatMap(KvEntry::getLongValue).ifPresent((inactivityTimeout) -> { - if (inactivityTimeout > 0) { - deviceStateData.getState().setInactivityTimeout(inactivityTimeout); - } - }); - } catch (Exception e) { - log.warn("[{}] Failed to fetch inactivity timeout from attribute", deviceStateData.getDeviceId(), e); - } + long inactivityTimeout = getEntryValue(ed, EntityKeyType.SERVER_ATTRIBUTE, INACTIVITY_TIMEOUT, TimeUnit.SECONDS.toMillis(defaultInactivityTimeoutInSec)); + deviceStateData.getState().setInactivityTimeout(inactivityTimeout); } } diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index bc0e067442..b33b7162c4 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.state; -import com.google.common.util.concurrent.Futures; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -27,8 +26,6 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; -import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.TsValue; @@ -40,19 +37,14 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import java.util.Map; -import java.util.Optional; import java.util.UUID; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.willReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; -import static org.mockito.Mockito.when; -import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; import static org.thingsboard.server.service.state.DefaultDeviceStateService.INACTIVITY_TIMEOUT; @RunWith(MockitoJUnitRunner.class) @@ -104,21 +96,35 @@ public class DefaultDeviceStateServiceTest { @Test public void givenPersistToTelemetryAndDefaultInactivityTimeoutFetched_whenTransformingToDeviceStateData_thenTryGetInactivityFromAttribute() { var defaultInactivityTimeoutInSec = 60L; + var latest = + Map.of( + EntityKeyType.TIME_SERIES, Map.of(INACTIVITY_TIMEOUT, new TsValue(0, Long.toString(defaultInactivityTimeoutInSec * 1000))), + EntityKeyType.SERVER_ATTRIBUTE, Map.of(INACTIVITY_TIMEOUT, new TsValue(0, Long.toString(5000L))) + ); + + process(latest, defaultInactivityTimeoutInSec); + } + + @Test + public void givenPersistToTelemetryAndNoInactivityTimeoutFetchedFromTimeSeries_whenTransformingToDeviceStateData_thenTryGetInactivityFromAttribute() { + var defaultInactivityTimeoutInSec = 60L; + var latest = + Map.of( + EntityKeyType.SERVER_ATTRIBUTE, Map.of(INACTIVITY_TIMEOUT, new TsValue(0, Long.toString(5000L))) + ); + + process(latest, defaultInactivityTimeoutInSec); + } + + private void process(Map> latest, long defaultInactivityTimeoutInSec) { service.setDefaultInactivityTimeoutInSec(defaultInactivityTimeoutInSec); service.setPersistToTelemetry(true); var deviceUuid = UUID.randomUUID(); var deviceId = new DeviceId(deviceUuid); - when(attributesService.find(any(), any(), anyString(), anyString())) - .thenReturn(Futures.immediateFuture(Optional.of(new BaseAttributeKvEntry(0, new LongDataEntry(INACTIVITY_TIMEOUT, 5000L))))); - - var latest = - Map.of(EntityKeyType.TIME_SERIES, Map.of(INACTIVITY_TIMEOUT, new TsValue(0, Long.toString(defaultInactivityTimeoutInSec * 1000)))); DeviceStateData deviceStateData = service.toDeviceStateData(new EntityData(deviceId, latest, Map.of()), new DeviceIdInfo(TenantId.SYS_TENANT_ID.getId(), UUID.randomUUID(), deviceUuid)); - Mockito.verify(attributesService, times(1)).find(TenantId.SYS_TENANT_ID, deviceId, SERVER_SCOPE, INACTIVITY_TIMEOUT); - Assert.assertEquals(5000L, deviceStateData.getState().getInactivityTimeout()); } From 03aafd4c3ca0065d6244ca473cbc9ed63b3b9bad Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 9 Nov 2022 13:28:42 +0100 Subject: [PATCH 077/226] added TbServiceQueue asset validation and upgrade improvements --- .../data/upgrade/3.4.1/schema_update_before.sql | 9 +++++---- .../server/controller/AssetController.java | 5 ----- .../entitiy/asset/DefaultTbAssetService.java | 14 ++++++++++++++ .../install/SqlDatabaseUpgradeService.java | 15 +++++---------- .../server/common/data/util}/TbPair.java | 2 +- .../thingsboard/server/dao/asset/AssetDao.java | 2 +- .../server/dao/sql/asset/AssetRepository.java | 4 ++-- .../server/dao/sql/asset/JpaAssetDao.java | 2 +- 8 files changed, 29 insertions(+), 24 deletions(-) rename {dao/src/main/java/org/thingsboard/server/dao/model/sql => common/data/src/main/java/org/thingsboard/server/common/data/util}/TbPair.java (94%) diff --git a/application/src/main/data/upgrade/3.4.1/schema_update_before.sql b/application/src/main/data/upgrade/3.4.1/schema_update_before.sql index 59566e42b5..27f772aba3 100644 --- a/application/src/main/data/upgrade/3.4.1/schema_update_before.sql +++ b/application/src/main/data/upgrade/3.4.1/schema_update_before.sql @@ -37,9 +37,10 @@ CREATE OR REPLACE PROCEDURE update_asset_profiles() LANGUAGE plpgsql AS $$ BEGIN -UPDATE asset as a SET asset_profile_id = p.id - FROM - (SELECT id, tenant_id, name from asset_profile) as p -WHERE a.asset_profile_id IS NULL AND p.tenant_id = a.tenant_id AND a.type = p.name; + UPDATE asset a SET asset_profile_id = COALESCE( + (SELECT id from asset_profile p WHERE p.tenant_id = a.tenant_id AND a.type = p.name), + (SELECT id from asset_profile p WHERE p.tenant_id = a.tenant_id AND p.name = 'default') + ) + WHERE a.asset_profile_id IS NULL; END; $$; diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index 8450c67ea3..968829b65a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -38,7 +38,6 @@ import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; @@ -86,7 +85,6 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHO import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.EdgeController.EDGE_ID; -import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE; @RestController @TbCoreComponent @@ -148,9 +146,6 @@ public class AssetController extends BaseController { @RequestMapping(value = "/asset", method = RequestMethod.POST) @ResponseBody public Asset saveAsset(@ApiParam(value = "A JSON value representing the asset.") @RequestBody Asset asset) throws Exception { - if (TB_SERVICE_QUEUE.equals(asset.getType())) { - throw new ThingsboardException("Unable to save asset with type " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS); - } asset.setTenantId(getTenantId()); checkEntity(asset.getId(), asset, Resource.ASSET); return tbAssetService.save(asset, getCurrentUser()); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java index 118dfe3b58..abaef626b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java @@ -22,8 +22,10 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; @@ -31,20 +33,32 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import org.thingsboard.server.service.profile.TbAssetProfileCache; import java.util.List; +import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE; + @Service @AllArgsConstructor public class DefaultTbAssetService extends AbstractTbEntityService implements TbAssetService { private final AssetService assetService; + private final TbAssetProfileCache assetProfileCache; @Override public Asset save(Asset asset, User user) throws Exception { ActionType actionType = asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = asset.getTenantId(); try { + if (TB_SERVICE_QUEUE.equals(asset.getType())) { + throw new ThingsboardException("Unable to save asset with type " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } else if (asset.getAssetProfileId() != null) { + AssetProfile assetProfile = assetProfileCache.get(tenantId, asset.getAssetProfileId()); + if (assetProfile != null && TB_SERVICE_QUEUE.equals(assetProfile.getName())) { + throw new ThingsboardException("Unable to save asset with profile " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + } Asset savedAsset = checkNotNull(assetService.saveAsset(asset)); autoCommit(user, savedAsset.getId()); notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedAsset.getId(), savedAsset, diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index d96475ad31..80665d544f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -39,7 +39,7 @@ import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.dao.model.sql.TbPair; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.sql.tenant.TenantRepository; import org.thingsboard.server.dao.tenant.TenantService; @@ -59,9 +59,7 @@ import java.sql.SQLSyntaxErrorException; import java.sql.SQLWarning; import java.sql.Statement; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -629,11 +627,11 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService conn.createStatement().execute("DELETE FROM asset a WHERE NOT exists(SELECT id FROM tenant WHERE id = a.tenant_id);"); log.info("Creating default asset profiles..."); - List> futures = new ArrayList<>(); PageLink pageLink = new PageLink(1000); PageData tenantIds; do { + List> futures = new ArrayList<>(); tenantIds = tenantService.findTenantsIds(pageLink); for (TenantId tenantId : tenantIds.getData()) { futures.add(dbUpgradeExecutor.submit(() -> { @@ -642,13 +640,14 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService } catch (Exception e) {} })); } + Futures.allAsList(futures).get(); pageLink = pageLink.nextPageLink(); } while (tenantIds.hasNext()); pageLink = new PageLink(1000); PageData> pairs; - Set tenants = new HashSet<>(); do { + List> futures = new ArrayList<>(); pairs = assetDao.getAllAssetTypes(pageLink); for (TbPair pair : pairs.getData()) { TenantId tenantId = new TenantId(pair.getFirst()); @@ -661,17 +660,13 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService })); } } + Futures.allAsList(futures).get(); pageLink = pageLink.nextPageLink(); } while (pairs.hasNext()); - Futures.allAsList(futures).get(); - log.info("Updating asset profiles..."); conn.createStatement().execute("call update_asset_profiles()"); - conn.createStatement().execute("UPDATE asset a SET asset_profile_id = " + - "(SELECT id FROM asset_profile ap WHERE ap.tenant_id = a.tenant_id AND name='default') WHERE a.asset_profile_id IS NULL;"); - schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.1", "schema_update_after.sql"); loadSql(schemaUpdateFile, conn); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbPair.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java similarity index 94% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/TbPair.java rename to common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java index 4cea485575..c7dba0c848 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbPair.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.model.sql; +package org.thingsboard.server.common.data.util; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java index d17181cffa..46a1a1bac5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.TenantEntityDao; -import org.thingsboard.server.dao.model.sql.TbPair; +import org.thingsboard.server.common.data.util.TbPair; import java.util.List; import java.util.Optional; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index cbb68b6ccc..aec62f0021 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -23,7 +23,7 @@ import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.AssetEntity; import org.thingsboard.server.dao.model.sql.AssetInfoEntity; -import org.thingsboard.server.dao.model.sql.TbPair; +import org.thingsboard.server.common.data.util.TbPair; import java.util.List; import java.util.UUID; @@ -197,7 +197,7 @@ public interface AssetRepository extends JpaRepository, Expor @Query("SELECT externalId FROM AssetEntity WHERE id = :id") UUID getExternalIdById(@Param("id") UUID id); - @Query(value = "SELECT DISTINCT new org.thingsboard.server.dao.model.sql.TbPair(a.tenantId , a.type) FROM AssetEntity a") + @Query(value = "SELECT DISTINCT new org.thingsboard.server.common.data.util.TbPair(a.tenantId , a.type) FROM AssetEntity a") Page> getAllAssetTypes(Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index a0c70428a5..b0e1921e87 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -33,7 +33,7 @@ import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.model.sql.AssetEntity; import org.thingsboard.server.dao.model.sql.AssetInfoEntity; -import org.thingsboard.server.dao.model.sql.TbPair; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; import org.thingsboard.server.dao.util.SqlDao; From 4ca140f306a026b6315f0de9f5f69cc6a26a4143 Mon Sep 17 00:00:00 2001 From: dlandiak Date: Wed, 9 Nov 2022 15:01:15 +0200 Subject: [PATCH 078/226] device state svc refactoring --- .../state/DefaultDeviceStateService.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index 4aaba9c6ea..ed9949a379 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -642,6 +642,11 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService Date: Wed, 9 Nov 2022 15:30:45 +0200 Subject: [PATCH 079/226] Added RPC_REQUEST and RPC_RESPONSE to the front-end --- ui-ngx/src/app/shared/models/edge.models.ts | 10 ++++++++-- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/shared/models/edge.models.ts b/ui-ngx/src/app/shared/models/edge.models.ts index 59f9d54318..6a84b6d29c 100644 --- a/ui-ngx/src/app/shared/models/edge.models.ts +++ b/ui-ngx/src/app/shared/models/edge.models.ts @@ -77,7 +77,9 @@ export enum EdgeEventActionType { UNASSIGNED_FROM_CUSTOMER = 'UNASSIGNED_FROM_CUSTOMER', RELATION_ADD_OR_UPDATE = 'RELATION_ADD_OR_UPDATE', RELATION_DELETED = 'RELATION_DELETED', - RPC_CALL = 'RPC_CALL', + RPC_CALL = 'RPC_CALL', // deprecated - to be removed in 4.x + RPC_CALL_REQUEST = 'RPC_CALL_REQUEST', + RPC_CALL_RESPONSE = 'RPC_CALL_RESPONSE', ALARM_ACK = 'ALARM_ACK', ALARM_CLEAR = 'ALARM_CLEAR', ASSIGNED_TO_EDGE = 'ASSIGNED_TO_EDGE', @@ -128,6 +130,8 @@ export const edgeEventActionTypeTranslations = new Map( 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 2b1eb11de7..c4031b6d0c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1791,6 +1791,8 @@ "action-type-relation-add-or-update": "Relation Add or Update", "action-type-relation-deleted": "Relation Deleted", "action-type-rpc-call": "RPC Call", + "action-type-rpc-call-request": "RPC Call Request", + "action-type-rpc-call-response": "RPC Call Response", "action-type-alarm-ack": "Alarm Ack", "action-type-alarm-clear": "Alarm Clear", "action-type-assigned-to-edge": "Assigned to Edge", From aefef78fda9556e4d887d2195a97cc9fc38493ef Mon Sep 17 00:00:00 2001 From: dlandiak Date: Wed, 9 Nov 2022 15:47:12 +0200 Subject: [PATCH 080/226] added defaultInactivityTimeoutMs param to device state svc --- .../service/state/DefaultDeviceStateService.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index ed9949a379..a223d97c96 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -157,6 +157,9 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService transformInactivityTimeout(ListenableFuture future) { return Futures.transformAsync(future, deviceStateData -> { - if (!persistToTelemetry || deviceStateData.getState().getInactivityTimeout() != TimeUnit.SECONDS.toMillis(defaultInactivityTimeoutInSec)) { + if (!persistToTelemetry || deviceStateData.getState().getInactivityTimeout() != defaultInactivityTimeoutMs) { return future; //fail fast } var attributesFuture = attributesService.find(TenantId.SYS_TENANT_ID, deviceStateData.getDeviceId(), SERVER_SCOPE, INACTIVITY_TIMEOUT); @@ -567,7 +570,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService Date: Wed, 9 Nov 2022 15:57:18 +0200 Subject: [PATCH 081/226] fix tests in device state svc --- .../server/service/state/DefaultDeviceStateService.java | 2 ++ .../server/service/state/DefaultDeviceStateServiceTest.java | 1 + 2 files changed, 3 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index a223d97c96..d52c712074 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -158,6 +158,8 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService> latest, long defaultInactivityTimeoutInSec) { service.setDefaultInactivityTimeoutInSec(defaultInactivityTimeoutInSec); + service.setDefaultInactivityTimeoutMs(defaultInactivityTimeoutInSec * 1000); service.setPersistToTelemetry(true); var deviceUuid = UUID.randomUUID(); From 323d3d51389b53e77b88e8dfe63f03ad97e3d531 Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Wed, 9 Nov 2022 16:33:43 +0200 Subject: [PATCH 082/226] fix bug for findAlarmDataByQueryForEntities method when sortOrder is null and textSearch used --- .../sql/query/DefaultAlarmQueryRepository.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index 69cb4a2d86..66219eb9ba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -140,6 +140,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { selectPart.append(" a.originator_id as entity_id "); } EntityDataSortOrder sortOrder = pageLink.getSortOrder(); + String textSearchQuery = buildTextSearchQuery(ctx, query.getAlarmFields(), pageLink.getTextSearch()); if (sortOrder != null && sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) { String sortOrderKey = sortOrder.getKey().getKey(); sortPart.append(alarmFieldColumnMap.getOrDefault(sortOrderKey, sortOrderKey)) @@ -166,7 +167,11 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { } joinPart.append(" as e(id, priority)) e "); if (pageLink.isSearchPropagatedAlarms()) { - joinPart.append("on ea.entity_id = e.id"); + if (textSearchQuery.isEmpty()) { + joinPart.append("on ea.entity_id = e.id"); + } else { + joinPart.append("on a.entity_id = e.id"); + } } else { joinPart.append("on a.originator_id = e.id"); } @@ -230,13 +235,11 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { } } - String textSearchQuery = buildTextSearchQuery(ctx, query.getAlarmFields(), pageLink.getTextSearch()); - String mainQuery; - if (!textSearchQuery.isEmpty()) { - mainQuery = selectPart.toString() + fromPart.toString() + wherePart.toString(); - mainQuery = String.format("select * from (%s) a %s WHERE %s", mainQuery, joinPart, textSearchQuery); + String mainQuery = String.format("%s%s", selectPart, fromPart); + if (textSearchQuery.isEmpty()) { + mainQuery += String.format("%s%s", joinPart, wherePart); } else { - mainQuery = selectPart.toString() + fromPart.toString() + joinPart.toString() + wherePart.toString(); + mainQuery = String.format("select * from (%s%s) a %s WHERE %s", mainQuery, wherePart, joinPart, textSearchQuery); } String countQuery = String.format("select count(*) from (%s) result", mainQuery); long queryTs = System.currentTimeMillis(); From 1b5d988d71d3a76c67341f8ec2e10dc887746367 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 9 Nov 2022 17:43:47 +0200 Subject: [PATCH 083/226] Improve upgrade from 3.4.1 to clear assets and repositorySettings cache --- .../service/install/update/DefaultCacheCleanupService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java index 9af49f4a7c..6e67876dc4 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java @@ -73,6 +73,11 @@ public class DefaultCacheCleanupService implements CacheCleanupService { log.info("Clear cache to upgrade from version 3.3.4 to 3.4.0 ..."); clearAll(); break; + case "3.4.1": + log.info("Clear cache to upgrade from version 3.4.1 to 3.4.2 ..."); + clearCacheByName("assets"); + clearCacheByName("repositorySettings"); + break; default: //Do nothing, since cache cleanup is optional. } From 385c3acaeed617d216a3501cf5567caacab9cea3 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 9 Nov 2022 17:56:38 +0200 Subject: [PATCH 084/226] UI: Fixed dashboard layout function setFixedLayout --- ...ge-dashboard-layouts-dialog.component.html | 22 ++++++++----- ...ge-dashboard-layouts-dialog.component.scss | 4 --- ...nage-dashboard-layouts-dialog.component.ts | 31 ++++++------------- 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html index 98c5226228..74e7db937b 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html @@ -65,12 +65,15 @@ mat-raised-button color="primary" class="tb-layout-button" - [matTooltip]="layoutButtonText('main')" - matTooltipPosition="above" - matTooltipClass="tb-layout-button-tooltip" + (mouseover)="mainLayoutTooltip.show()" + (mouseleave)="mainLayoutTooltip.hide()" (click)="setFixedLayout('main')" [ngClass]="layoutButtonClass('main', true)"> - {{ (layoutsFormGroup.value.right ? 'layout.left' : 'layout.main') | translate }} + + {{ (layoutsFormGroup.value.right ? 'layout.left' : 'layout.main') | translate }} +

- {{ 'layout.right' | translate }} + + {{ 'layout.right' | translate }} +
{ - elementToDisable.disabled = false; - }, 250); - } - - if (this.layoutsFormGroup.get('type').value === LayoutWidthType.FIXED) { + if (this.layoutsFormGroup.get('type').value === LayoutWidthType.FIXED && this.layoutsFormGroup.get('right').value) { this.layoutsFormGroup.get('fixedLayout').setValue(layout); } } From 5ff8144f8db2f5f2fe95e9bb6e0658faeab78367 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 9 Nov 2022 18:31:08 +0200 Subject: [PATCH 085/226] Code cleanup --- .../rpc/processor/DeviceEdgeProcessor.java | 16 +-------- common/edge-api/src/main/proto/edge.proto | 10 +++--- .../engine/edge/AbstractTbMsgPushNode.java | 5 ++- .../rule/engine/rpc/TbSendRPCReplyNode.java | 33 +++---------------- 4 files changed, 14 insertions(+), 50 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java index 959f5c5fe8..147bb454fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java @@ -369,36 +369,22 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { private ListenableFuture processDeviceRpcRequestFromEdge(TenantId tenantId, Edge edge, DeviceRpcCallMsg deviceRpcCallMsg) { DeviceId deviceId = new DeviceId(new UUID(deviceRpcCallMsg.getDeviceIdMSB(), deviceRpcCallMsg.getDeviceIdLSB())); - UUID requestUUID = new UUID(deviceRpcCallMsg.getRequestUuidMSB(), deviceRpcCallMsg.getRequestUuidLSB()); try { - ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.createObjectNode(); TbMsgMetaData metaData = new TbMsgMetaData(); String requestId = Integer.toString(deviceRpcCallMsg.getRequestId()); metaData.putValue("requestId", requestId); - metaData.putValue("requestUUID", requestUUID.toString()); metaData.putValue("serviceId", deviceRpcCallMsg.getServiceId()); metaData.putValue("sessionId", deviceRpcCallMsg.getSessionId()); - metaData.putValue("expirationTime", Long.toString(deviceRpcCallMsg.getExpirationTime())); - metaData.putValue("oneway", Boolean.toString(deviceRpcCallMsg.getOneway())); - metaData.putValue(DataConstants.PERSISTENT, Boolean.toString(deviceRpcCallMsg.getPersisted())); - - if (deviceRpcCallMsg.getRetries() > 0) { - metaData.putValue(DataConstants.RETRIES, Integer.toString(deviceRpcCallMsg.getRetries())); - } - metaData.putValue(DataConstants.EDGE_ID, edge.getId().toString()); - Device device = deviceService.findDeviceById(tenantId, deviceId); if (device != null) { metaData.putValue("deviceName", device.getName()); metaData.putValue("deviceType", device.getType()); metaData.putValue(DataConstants.DEVICE_ID, deviceId.getId().toString()); } - + ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.createObjectNode(); entityNode.put("method", deviceRpcCallMsg.getRequestMsg().getMethod()); entityNode.put("params", deviceRpcCallMsg.getRequestMsg().getParams()); - - entityNode.put(DataConstants.ADDITIONAL_INFO, deviceRpcCallMsg.getAdditionalInfo()); TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.TO_SERVER_RPC_REQUEST.name(), deviceId, null, metaData, TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, new TbQueueCallback() { diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index d70179cfe0..249f251238 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -430,11 +430,11 @@ message DeviceRpcCallMsg { bool oneway = 7; RpcRequestMsg requestMsg = 8; RpcResponseMsg responseMsg = 9; - bool persisted = 10; - int32 retries = 11; - string additionalInfo = 12; - string serviceId = 13; - string sessionId = 14; + optional bool persisted = 10; + optional int32 retries = 11; + optional string additionalInfo = 12; + optional string serviceId = 13; + optional string sessionId = 14; } message RpcRequestMsg { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java index 28dc64c068..f120276df3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java @@ -142,8 +142,11 @@ public abstract class AbstractTbMsgPushNode future = ctx.getEdgeEventService().saveAsync(edgeEvent); Futures.addCallback(future, new FutureCallback() { @Override From 1635d883a77c41ab4714f80030bf92594bb56fc0 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 9 Nov 2022 18:32:51 +0200 Subject: [PATCH 086/226] Rename variable --- .../service/edge/rpc/processor/DeviceEdgeProcessor.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java index 147bb454fa..4a517e19e9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java @@ -382,11 +382,11 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { metaData.putValue("deviceType", device.getType()); metaData.putValue(DataConstants.DEVICE_ID, deviceId.getId().toString()); } - ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.createObjectNode(); - entityNode.put("method", deviceRpcCallMsg.getRequestMsg().getMethod()); - entityNode.put("params", deviceRpcCallMsg.getRequestMsg().getParams()); + ObjectNode data = JacksonUtil.OBJECT_MAPPER.createObjectNode(); + data.put("method", deviceRpcCallMsg.getRequestMsg().getMethod()); + data.put("params", deviceRpcCallMsg.getRequestMsg().getParams()); TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.TO_SERVER_RPC_REQUEST.name(), deviceId, null, metaData, - TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); + TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(data)); tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { From 54f967e944b20ad5ea4b2e77ac67d6d5beacc34d Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 9 Nov 2022 18:35:18 +0200 Subject: [PATCH 087/226] Remove unused file --- .../rpc/processor/EdgeRpcRequestMetadata.java | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeRpcRequestMetadata.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeRpcRequestMetadata.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeRpcRequestMetadata.java deleted file mode 100644 index a5a71d0743..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeRpcRequestMetadata.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright © 2016-2022 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; - -import lombok.Data; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.TenantId; - -@Data -public class EdgeRpcRequestMetadata { - private final TenantId tenantId; - private final EdgeId edgeId; - private final DeviceId deviceId; -} From 7186632e5a37f6e63e14a144903c2ced6584c667 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 9 Nov 2022 23:24:09 +0100 Subject: [PATCH 088/226] lombok.copyableAnnotations += org.springframework.context.annotation.Lazy --- lombok.config | 1 + 1 file changed, 1 insertion(+) diff --git a/lombok.config b/lombok.config index d904701090..2299612bb9 100644 --- a/lombok.config +++ b/lombok.config @@ -1,2 +1,3 @@ config.stopbubbling = true lombok.anyconstructor.addconstructorproperties = true +lombok.copyableAnnotations += org.springframework.context.annotation.Lazy \ No newline at end of file From 1a9b8a1ebe27b232b3874af8e15f06b85c7ef64f Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 9 Nov 2022 23:28:34 +0100 Subject: [PATCH 089/226] JwtSettingsService workout: Lazy and Optional clusterService, correctness on first Install and upgrade, reload JWT on cluster notification, update jwt settings using existing id --- .../server/config/jwt/JwtSettingsService.java | 2 + .../config/jwt/JwtSettingsServiceDefault.java | 56 +++++++++++++------ .../server/controller/AdminController.java | 2 + .../queue/DefaultTbCoreConsumerService.java | 6 +- .../DefaultTbRuleEngineConsumerService.java | 3 +- .../processing/AbstractConsumerService.java | 22 +++++--- 6 files changed, 63 insertions(+), 28 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java index fb673fe50c..252b0a021c 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java @@ -19,6 +19,8 @@ public interface JwtSettingsService { JwtSettings getJwtSettings(); + void reloadJwtSettings(); + void createJwtAdminSettings(); JwtSettings saveJwtSettings(JwtSettings jwtSettings); diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java index 5c172c9c2d..d65edaa93b 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java @@ -19,7 +19,10 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; -import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; @@ -33,6 +36,7 @@ import javax.validation.ValidationException; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Objects; +import java.util.Optional; @Service @RequiredArgsConstructor @@ -42,24 +46,37 @@ public class JwtSettingsServiceDefault implements JwtSettingsService { static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; static final String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; static final String TB_ALLOW_DEFAULT_JWT_SIGNING_KEY = "TB_ALLOW_DEFAULT_JWT_SIGNING_KEY"; - + @Lazy private final AdminSettingsService adminSettingsService; - private final TbClusterService tbClusterService; - + @Lazy + private final Optional tbClusterService; private final JwtSettingsValidator jwtSettingsValidator; - + private final Environment environment; @Getter private final JwtSettings jwtSettings; + @Value("${install.upgrade:false}") + private boolean isUpgrade; @PostConstruct public void init() { - reloadJwtSettings(); + if (!isFirstInstall()) { + reloadJwtSettings(); + } + } + + private boolean isInstall() { + return environment.acceptsProfiles(Profiles.of("install")); } - void reloadJwtSettings() { + private boolean isFirstInstall() { + return isInstall() && !isUpgrade; + } + + @Override + public void reloadJwtSettings() { AdminSettings adminJwtSettings = findJwtAdminSettings(); if (adminJwtSettings != null) { - log.debug("Loading the JWT admin settings from database"); + log.info("Reloading the JWT admin settings from database"); JwtSettings jwtLoaded = mapAdminToJwtSettings(adminJwtSettings); jwtSettings.setRefreshTokenExpTime(jwtLoaded.getRefreshTokenExpTime()); jwtSettings.setTokenExpirationTime(jwtLoaded.getTokenExpirationTime()); @@ -67,7 +84,7 @@ public class JwtSettingsServiceDefault implements JwtSettingsService { jwtSettings.setTokenSigningKey(jwtLoaded.getTokenSigningKey()); } - if (hasDefaultTokenSigningKey()) { + if (hasDefaultTokenSigningKey() && !isFirstInstall()) { log.warn("JWT token signing key is default. This is a security issue. Please, consider to set unique value"); } } @@ -107,12 +124,20 @@ public class JwtSettingsServiceDefault implements JwtSettingsService { } @Override - public JwtSettings saveJwtSettings(JwtSettings jwtSettings){ + public JwtSettings saveJwtSettings(JwtSettings jwtSettings) { jwtSettingsValidator.validate(jwtSettings); - AdminSettings adminJwtSettings = mapJwtToAdminSettings(jwtSettings); + final AdminSettings adminJwtSettings = mapJwtToAdminSettings(jwtSettings); + final AdminSettings existedSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); + if (existedSettings != null) { + adminJwtSettings.setId(existedSettings.getId()); + } + log.info("Saving new JWT admin settings. From this moment, the JWT parameters from YAML and ENV will be ignored"); adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminJwtSettings); - tbClusterService.broadcastEntityStateChangeEvent(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, ComponentLifecycleEvent.UPDATED); + + if (!isInstall()) { + tbClusterService.orElseThrow().broadcastEntityStateChangeEvent(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, ComponentLifecycleEvent.UPDATED); + } reloadJwtSettings(); return getJwtSettings(); } @@ -122,12 +147,7 @@ public class JwtSettingsServiceDefault implements JwtSettingsService { } AdminSettings findJwtAdminSettings() { - try { - return adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); - } catch (InvalidDataAccessResourceUsageException ignored) { - log.debug("findAdminSettingsByKey is returning InvalidDataAccessResourceUsageException. This is an installation case when the database is not initialized yet"); - return null; - } + return adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); } /* diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 6b5977cac7..3ea4e5e13c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -22,6 +22,7 @@ import com.google.common.util.concurrent.MoreExecutors; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; @@ -68,6 +69,7 @@ public class AdminController extends BaseController { @Autowired private SystemSecurityService systemSecurityService; + @Lazy @Autowired private JwtSettingsService jwtSettingsService; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index ec58ac1aa9..d10c968364 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.config.jwt.JwtSettingsService; import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; @@ -143,8 +144,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService jwtSettingsService) { + super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService); this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer(); this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer(); this.firmwareStatesConsumer = tbCoreQueueFactory.createToOtaPackageStateServiceMsgConsumer(); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index d870af318e..dd97db3703 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -70,6 +70,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -126,7 +127,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< TbTenantProfileCache tenantProfileCache, TbApiUsageStateService apiUsageStateService, PartitionService partitionService, TbServiceInfoProvider serviceInfoProvider, QueueService queueService) { - super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer()); + super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), Optional.empty()); this.statisticsService = statisticsService; this.tbRuleEngineQueueFactory = tbRuleEngineQueueFactory; this.submitStrategyFactory = submitStrategyFactory; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index 47b7f3f9f9..df7f0922ea 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.config.jwt.JwtSettingsService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -76,11 +77,13 @@ public abstract class AbstractConsumerService> nfConsumer; + protected final Optional jwtSettingsService; + public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService, TbTenantProfileCache tenantProfileCache, TbDeviceProfileCache deviceProfileCache, TbAssetProfileCache assetProfileCache, TbApiUsageStateService apiUsageStateService, - PartitionService partitionService, TbQueueConsumer> nfConsumer) { + PartitionService partitionService, TbQueueConsumer> nfConsumer, Optional jwtSettingsService) { this.actorContext = actorContext; this.encodingService = encodingService; this.tenantProfileCache = tenantProfileCache; @@ -89,6 +92,7 @@ public abstract class AbstractConsumerService Date: Thu, 10 Nov 2022 09:35:06 +0200 Subject: [PATCH 090/226] Merge RPC request/response actions --- .../device/DeviceActorMessageProcessor.java | 2 +- .../service/edge/rpc/EdgeGrpcSession.java | 3 +- .../rpc/constructor/DeviceMsgConstructor.java | 35 +++++++------------ .../rpc/processor/DeviceEdgeProcessor.java | 21 +++-------- .../server/edge/BaseDeviceEdgeTest.java | 2 +- .../common/data/edge/EdgeEventActionType.java | 3 +- .../rule/engine/rpc/TbSendRPCReplyNode.java | 2 +- ui-ngx/src/app/shared/models/edge.models.ts | 10 ++---- .../assets/locale/locale.constant-en_US.json | 2 -- 9 files changed, 25 insertions(+), 55 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 9b95b188d2..8a365b4e56 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -827,7 +827,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { body.put("retries", msg.getRetries()); body.put("additionalInfo", msg.getAdditionalInfo()); - EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(tenantId, edgeId, EdgeEventType.DEVICE, EdgeEventActionType.RPC_CALL_REQUEST, deviceId, body); + EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(tenantId, edgeId, EdgeEventType.DEVICE, EdgeEventActionType.RPC_CALL, deviceId, body); return Futures.transform(systemContext.getEdgeEventService().saveAsync(edgeEvent), unused -> { systemContext.getClusterService().onEdgeEventUpdate(tenantId, edgeId); 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 aca2f8b562..957c699e32 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 @@ -463,8 +463,7 @@ public final class EdgeGrpcSession implements Closeable { case UNASSIGNED_FROM_CUSTOMER: case CREDENTIALS_REQUEST: case ENTITY_MERGE_REQUEST: - case RPC_CALL_REQUEST: - case RPC_CALL_RESPONSE: + case RPC_CALL: downlinkMsg = convertEntityEventToDownlink(edgeEvent); log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), this.sessionId, downlinkMsg); break; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java index ebea5d482f..522bcd3a4e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java @@ -21,7 +21,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; @@ -97,30 +96,22 @@ public class DeviceMsgConstructor { .setIdLSB(deviceId.getId().getLeastSignificantBits()).build(); } - public DeviceRpcCallMsg constructDeviceRpcRequestMsg(UUID deviceId, JsonNode body) { + public DeviceRpcCallMsg constructDeviceRpcCallMsg(UUID deviceId, JsonNode body) { DeviceRpcCallMsg.Builder builder = constructDeviceRpcMsg(deviceId, body); - - String method = body.get("method").asText(); - String params = body.get("params").asText(); - RpcRequestMsg.Builder requestBuilder = RpcRequestMsg.newBuilder(); - requestBuilder.setMethod(method); - requestBuilder.setParams(params); - builder.setRequestMsg(requestBuilder.build()); - - return builder.build(); - } - - public DeviceRpcCallMsg constructDeviceRpcResponseMsg(UUID deviceId, JsonNode body) { - DeviceRpcCallMsg.Builder builder = constructDeviceRpcMsg(deviceId, body); - - RpcResponseMsg.Builder responseBuilder = RpcResponseMsg.newBuilder(); - if (body.has("error")) { - responseBuilder.setError(body.get("error").asText()); + if (body.has("error") || body.has("response")) { + RpcResponseMsg.Builder responseBuilder = RpcResponseMsg.newBuilder(); + if (body.has("error")) { + responseBuilder.setError(body.get("error").asText()); + } else { + responseBuilder.setResponse(body.get("response").asText()); + } + builder.setResponseMsg(responseBuilder.build()); } else { - responseBuilder.setResponse(body.get("response").asText()); + RpcRequestMsg.Builder requestBuilder = RpcRequestMsg.newBuilder(); + requestBuilder.setMethod(body.get("method").asText()); + requestBuilder.setParams(body.get("params").asText()); + builder.setRequestMsg(requestBuilder.build()); } - builder.setResponseMsg(responseBuilder.build()); - return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java index 4a517e19e9..81e7970611 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java @@ -451,10 +451,8 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { .build(); } break; - case RPC_CALL_REQUEST: - return convertRpcCallRequestEventToDownlink(edgeEvent); - case RPC_CALL_RESPONSE: - return convertRpcCallResponseEventToDownlink(edgeEvent); + case RPC_CALL: + return convertRpcCallEventToDownlink(edgeEvent); case CREDENTIALS_REQUEST: return convertCredentialsRequestEventToDownlink(edgeEvent); case ENTITY_MERGE_REQUEST: @@ -463,20 +461,11 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { return downlinkMsg; } - private DownlinkMsg convertRpcCallRequestEventToDownlink(EdgeEvent edgeEvent) { - log.trace("Executing convertRpcCallRequestEventToDownlink, edgeEvent [{}]", edgeEvent); + private DownlinkMsg convertRpcCallEventToDownlink(EdgeEvent edgeEvent) { + log.trace("Executing convertRpcCallEventToDownlink, edgeEvent [{}]", edgeEvent); return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceRpcCallMsg(deviceMsgConstructor.constructDeviceRpcRequestMsg(edgeEvent.getEntityId(), edgeEvent.getBody())) - .build(); - } - - private DownlinkMsg convertRpcCallResponseEventToDownlink(EdgeEvent edgeEvent) { - log.trace("Executing convertRpcCallResponseEventToDownlink, edgeEvent [{}]", edgeEvent); - DeviceRpcCallMsg deviceRpcCallMsg = deviceMsgConstructor.constructDeviceRpcResponseMsg(edgeEvent.getEntityId(), edgeEvent.getBody()); - return DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceRpcCallMsg(deviceRpcCallMsg) + .addDeviceRpcCallMsg(deviceMsgConstructor.constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody())) .build(); } diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java index 8e8c616d7a..be8089e4c0 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java @@ -512,7 +512,7 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest { body.put("method", "test_method"); body.put("params", "{\"param1\":\"value1\"}"); - EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.RPC_CALL_REQUEST, + EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.RPC_CALL, device.getId().getId(), EdgeEventType.DEVICE, body); edgeImitator.expectMessageAmount(1); edgeEventService.saveAsync(edgeEvent).get(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java index 7fae6c56b1..bba8767fca 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java @@ -28,8 +28,7 @@ public enum EdgeEventActionType { UNASSIGNED_FROM_CUSTOMER, RELATION_ADD_OR_UPDATE, RELATION_DELETED, - RPC_CALL_REQUEST, - RPC_CALL_RESPONSE, + RPC_CALL, ALARM_ACK, ALARM_CLEAR, ASSIGNED_TO_EDGE, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java index cf0ebefc01..db3953f6cb 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java @@ -96,7 +96,7 @@ public class TbSendRPCReplyNode implements TbNode { EdgeId edgeId = new EdgeId(UUID.fromString(msg.getMetaData().getValue(DataConstants.EDGE_ID))); DeviceId deviceId = new DeviceId(UUID.fromString(msg.getMetaData().getValue(DataConstants.DEVICE_ID))); EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(ctx.getTenantId(), edgeId, EdgeEventType.DEVICE, - EdgeEventActionType.RPC_CALL_RESPONSE, deviceId, JacksonUtil.OBJECT_MAPPER.valueToTree(body)); + EdgeEventActionType.RPC_CALL, deviceId, JacksonUtil.OBJECT_MAPPER.valueToTree(body)); ListenableFuture future = ctx.getEdgeEventService().saveAsync(edgeEvent); Futures.addCallback(future, new FutureCallback() { @Override diff --git a/ui-ngx/src/app/shared/models/edge.models.ts b/ui-ngx/src/app/shared/models/edge.models.ts index 6a84b6d29c..59f9d54318 100644 --- a/ui-ngx/src/app/shared/models/edge.models.ts +++ b/ui-ngx/src/app/shared/models/edge.models.ts @@ -77,9 +77,7 @@ export enum EdgeEventActionType { UNASSIGNED_FROM_CUSTOMER = 'UNASSIGNED_FROM_CUSTOMER', RELATION_ADD_OR_UPDATE = 'RELATION_ADD_OR_UPDATE', RELATION_DELETED = 'RELATION_DELETED', - RPC_CALL = 'RPC_CALL', // deprecated - to be removed in 4.x - RPC_CALL_REQUEST = 'RPC_CALL_REQUEST', - RPC_CALL_RESPONSE = 'RPC_CALL_RESPONSE', + RPC_CALL = 'RPC_CALL', ALARM_ACK = 'ALARM_ACK', ALARM_CLEAR = 'ALARM_CLEAR', ASSIGNED_TO_EDGE = 'ASSIGNED_TO_EDGE', @@ -130,8 +128,6 @@ export const edgeEventActionTypeTranslations = new Map( 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 17b4533b03..3f3f70860f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1791,8 +1791,6 @@ "action-type-relation-add-or-update": "Relation Add or Update", "action-type-relation-deleted": "Relation Deleted", "action-type-rpc-call": "RPC Call", - "action-type-rpc-call-request": "RPC Call Request", - "action-type-rpc-call-response": "RPC Call Response", "action-type-alarm-ack": "Alarm Ack", "action-type-alarm-clear": "Alarm Clear", "action-type-assigned-to-edge": "Assigned to Edge", From 15b26f4317f8675b73bd2cfc0ff55336c633daf1 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 10 Nov 2022 10:37:14 +0200 Subject: [PATCH 091/226] Remove Nullable annotation --- .../org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java index db3953f6cb..796aa5658c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java @@ -20,7 +20,6 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.checkerframework.checker.nullness.qual.Nullable; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -98,9 +97,9 @@ public class TbSendRPCReplyNode implements TbNode { EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(ctx.getTenantId(), edgeId, EdgeEventType.DEVICE, EdgeEventActionType.RPC_CALL, deviceId, JacksonUtil.OBJECT_MAPPER.valueToTree(body)); ListenableFuture future = ctx.getEdgeEventService().saveAsync(edgeEvent); - Futures.addCallback(future, new FutureCallback() { + Futures.addCallback(future, new FutureCallback<>() { @Override - public void onSuccess(@Nullable Void result) { + public void onSuccess(Void result) { ctx.onEdgeEventUpdate(ctx.getTenantId(), edgeId); ctx.tellSuccess(msg); } From d8a609344289f9ae7de77712cbbd1793b1a912a1 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 10 Nov 2022 11:36:46 +0200 Subject: [PATCH 092/226] Added TbSendRPCReplyNodeTest --- .../server/edge/BaseDeviceEdgeTest.java | 4 + .../rule/engine/rpc/TbSendRPCReplyNode.java | 13 +- .../engine/rpc/TbSendRPCReplyNodeTest.java | 119 ++++++++++++++++++ 3 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java index be8089e4c0..ffe21ae961 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java @@ -511,6 +511,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest { body.put("expirationTime", System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(10)); body.put("method", "test_method"); body.put("params", "{\"param1\":\"value1\"}"); + body.put("persisted", true); + body.put("retries", 2); EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.RPC_CALL, device.getId().getId(), EdgeEventType.DEVICE, body); @@ -523,6 +525,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest { Assert.assertTrue(latestMessage instanceof DeviceRpcCallMsg); DeviceRpcCallMsg latestDeviceRpcCallMsg = (DeviceRpcCallMsg) latestMessage; Assert.assertEquals("test_method", latestDeviceRpcCallMsg.getRequestMsg().getMethod()); + Assert.assertTrue(latestDeviceRpcCallMsg.getPersisted()); + Assert.assertEquals(2, latestDeviceRpcCallMsg.getRetries()); } private void sendAttributesRequestAndVerify(Device device, String scope, String attributesDataStr, String expectedKey, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java index 796aa5658c..405a73944e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java @@ -87,13 +87,22 @@ public class TbSendRPCReplyNode implements TbNode { } private void saveRpcResponseToEdgeQueue(TbContext ctx, TbMsg msg, String serviceIdStr, String sessionIdStr, String requestIdStr) { + EdgeId edgeId; + DeviceId deviceId; + try { + edgeId = new EdgeId(UUID.fromString(msg.getMetaData().getValue(DataConstants.EDGE_ID))); + deviceId = new DeviceId(UUID.fromString(msg.getMetaData().getValue(DataConstants.DEVICE_ID))); + } catch (Exception e) { + String errMsg = String.format("[%s] Failed to parse edgeId or deviceId from metadata %s!", ctx.getTenantId(), msg.getMetaData()); + ctx.tellFailure(msg, new RuntimeException(errMsg)); + return; + } + ObjectNode body = JacksonUtil.OBJECT_MAPPER.createObjectNode(); body.put("serviceId", serviceIdStr); body.put("sessionId", sessionIdStr); body.put("requestId", requestIdStr); body.put("response", msg.getData()); - EdgeId edgeId = new EdgeId(UUID.fromString(msg.getMetaData().getValue(DataConstants.EDGE_ID))); - DeviceId deviceId = new DeviceId(UUID.fromString(msg.getMetaData().getValue(DataConstants.DEVICE_ID))); EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(ctx.getTenantId(), edgeId, EdgeEventType.DEVICE, EdgeEventActionType.RPC_CALL, deviceId, JacksonUtil.OBJECT_MAPPER.valueToTree(body)); ListenableFuture future = ctx.getEdgeEventService().saveAsync(edgeEvent); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java new file mode 100644 index 0000000000..095505aff3 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java @@ -0,0 +1,119 @@ +/** + * Copyright © 2016-2022 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.rule.engine.rpc; + +import com.google.common.util.concurrent.SettableFuture; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.api.RuleEngineRpcService; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.dao.edge.EdgeEventService; + +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class TbSendRPCReplyNodeTest { + + private static final String DUMMY_SERVICE_ID = "testServiceId"; + private static final int DUMMY_REQUEST_ID = 0; + private static final UUID DUMMY_SESSION_ID = UUID.randomUUID(); + private static final String DUMMY_DATA = "{\"key\":\"value\"}"; + + TbSendRPCReplyNode node; + + private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + private final DeviceId deviceId = new DeviceId(UUID.randomUUID()); + + @Mock + private TbContext ctx; + + @Mock + private RuleEngineRpcService rpcService; + + @Mock + private EdgeEventService edgeEventService; + + @Mock + private ListeningExecutor listeningExecutor; + + @Before + public void setUp() throws TbNodeException { + node = new TbSendRPCReplyNode(); + TbSendRpcReplyNodeConfiguration config = new TbSendRpcReplyNodeConfiguration().defaultConfiguration(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + } + + @Test + public void sendReplyToTransport() { + Mockito.when(ctx.getRpcService()).thenReturn(rpcService); + + + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, getDefaultMetadata(), + TbMsgDataType.JSON, DUMMY_DATA, null, null); + + node.onMsg(ctx, msg); + + verify(rpcService).sendRpcReplyToDevice(DUMMY_SERVICE_ID, DUMMY_SESSION_ID, DUMMY_REQUEST_ID, DUMMY_DATA); + verify(edgeEventService, never()).saveAsync(any()); + } + + @Test + public void sendReplyToEdgeQueue() { + Mockito.when(ctx.getTenantId()).thenReturn(tenantId); + Mockito.when(ctx.getEdgeEventService()).thenReturn(edgeEventService); + Mockito.when(edgeEventService.saveAsync(any())).thenReturn(SettableFuture.create()); + Mockito.when(ctx.getDbCallbackExecutor()).thenReturn(listeningExecutor); + + TbMsgMetaData defaultMetadata = getDefaultMetadata(); + defaultMetadata.putValue(DataConstants.EDGE_ID, UUID.randomUUID().toString()); + defaultMetadata.putValue(DataConstants.DEVICE_ID, UUID.randomUUID().toString()); + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, defaultMetadata, + TbMsgDataType.JSON, DUMMY_DATA, null, null); + + node.onMsg(ctx, msg); + + verify(edgeEventService).saveAsync(any()); + verify(rpcService, never()).sendRpcReplyToDevice(DUMMY_SERVICE_ID, DUMMY_SESSION_ID, DUMMY_REQUEST_ID, DUMMY_DATA); + } + + private TbMsgMetaData getDefaultMetadata() { + TbSendRpcReplyNodeConfiguration config = new TbSendRpcReplyNodeConfiguration().defaultConfiguration(); + TbMsgMetaData metadata = new TbMsgMetaData(); + metadata.putValue(config.getServiceIdMetaDataAttribute(), DUMMY_SERVICE_ID); + metadata.putValue(config.getSessionIdMetaDataAttribute(), DUMMY_SESSION_ID.toString()); + metadata.putValue(config.getRequestIdMetaDataAttribute(), Integer.toString(DUMMY_REQUEST_ID)); + return metadata; + } +} From 1f1c8fb013ea4ecbdb3480ed0e5401e7676a495f Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 10 Nov 2022 10:41:34 +0100 Subject: [PATCH 093/226] saveJwtSettings returns JwtTokenPair in AdminController --- .../server/controller/AdminController.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 3ea4e5e13c..2712f58439 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -43,6 +43,9 @@ import org.thingsboard.server.config.jwt.JwtSettings; import org.thingsboard.server.config.jwt.JwtSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.model.JwtTokenPair; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.system.SystemSecurityService; @@ -73,6 +76,10 @@ public class AdminController extends BaseController { @Autowired private JwtSettingsService jwtSettingsService; + @Lazy + @Autowired + private JwtTokenFactory tokenFactory; + @Autowired private EntitiesVersionControlService versionControlService; @@ -175,19 +182,20 @@ public class AdminController extends BaseController { } } - @ApiOperation(value = "Update JWT Settings (saveSecuritySettings)", + @ApiOperation(value = "Update JWT Settings (saveJwtSettings)", notes = "Updates the JWT Settings object that contains JWT token policy, etc. The tokenSigningKey field is a Base64 encoded string." + SYSTEM_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/jwtSettings", method = RequestMethod.POST) @ResponseBody - public JwtSettings saveJwtSettings( + public JwtTokenPair saveJwtSettings( @ApiParam(value = "A JSON value representing the JWT Settings.") @RequestBody JwtSettings jwtSettings) throws ThingsboardException { try { - accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); - jwtSettings = checkNotNull(jwtSettingsService.saveJwtSettings(jwtSettings)); - return jwtSettings; + SecurityUser securityUser = getCurrentUser(); + accessControlService.checkPermission(securityUser, Resource.ADMIN_SETTINGS, Operation.WRITE); + checkNotNull(jwtSettingsService.saveJwtSettings(jwtSettings)); + return tokenFactory.createTokenPair(securityUser); } catch (Exception e) { throw handleException(e); } From 6223307cd8eb7e5ab7d5c022762a2926ef658f75 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Thu, 10 Nov 2022 17:04:48 +0200 Subject: [PATCH 094/226] Add validation for Asset Profile FK in Rule Chains --- .../org/thingsboard/server/dao/rule/BaseRuleChainService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 420739e37c..1d11d34b2d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -731,6 +731,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_rule_chain_device_profile")) { throw new DataValidationException("The rule chain referenced by the device profiles cannot be deleted!"); + } else if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_rule_chain_asset_profile")) { + throw new DataValidationException("The rule chain referenced by the asset profiles cannot be deleted!"); } else { throw t; } From 1b191acace1f28bf6ab8b0643384cdb3041ff79f Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 10 Nov 2022 17:23:46 +0200 Subject: [PATCH 095/226] RestClient - add getRuleChains by type. Updated edge_root_rule_chain - remove Success from RPC Call from device to Push to cloud --- .../edge_management/rule_chains/edge_root_rule_chain.json | 5 ----- .../main/java/org/thingsboard/rest/client/RestClient.java | 7 ++++++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json index ae2e43fa73..717fc74715 100644 --- a/application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json +++ b/application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json @@ -174,11 +174,6 @@ "fromIndex": 3, "toIndex": 7, "type": "Timeseries Updated" - }, - { - "fromIndex": 4, - "toIndex": 7, - "type": "Success" } ], "ruleChainConnections": null diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 21a18534ed..fabc35bc49 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -2032,10 +2032,15 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } public PageData getRuleChains(PageLink pageLink) { + return getRuleChains(RuleChainType.CORE, pageLink); + } + + public PageData getRuleChains(RuleChainType ruleChainType, PageLink pageLink) { Map params = new HashMap<>(); + params.put("type", ruleChainType.name()); addPageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/ruleChains?" + getUrlParams(pageLink), + baseURL + "/api/ruleChains?type={type}&" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { From 1ab4c03622544e61a00efc4d43a715cd7ae1fd23 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 10 Nov 2022 18:30:05 +0200 Subject: [PATCH 096/226] fixed license format --- .../src/test/resources/testNG.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/msa/black-box-tests/src/test/resources/testNG.xml b/msa/black-box-tests/src/test/resources/testNG.xml index c5ad55c9d1..45e93f76f1 100644 --- a/msa/black-box-tests/src/test/resources/testNG.xml +++ b/msa/black-box-tests/src/test/resources/testNG.xml @@ -1,4 +1,21 @@ + From b95f1c95e09c23a0c552c660f25521440233136c Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 10 Nov 2022 18:00:54 +0100 Subject: [PATCH 097/226] jwt settings workout on review feedback --- .../config/jwt/JwtSettingsServiceDefault.java | 6 +++++- .../config/jwt/JwtSettingsValidator.java | 19 ++++++++++++++----- .../processing/AbstractConsumerService.java | 1 + 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java index d65edaa93b..f64b7c4658 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java @@ -164,7 +164,11 @@ public class JwtSettingsServiceDefault implements JwtSettingsService { if (isAllowedDefaultJwtSigningKey()) { log.warn("Default JWT signing key is allowed. This is a security issue. Please, consider to set a strong key in admin settings"); } else { - String message = "Please, set a unique signing key with env variable JWT_TOKEN_SIGNING_KEY. Key is a Base64 encoded phrase. This will require to generate new tokens for all users and API that uses JWT tokens. To allow insecure JWS use TB_ALLOW_DEFAULT_JWT_SIGNING_KEY=true"; + String message = "UPGRADE ERROR. YOUR ACTION REQUIRED. Please, set a unique signing key with env variable JWT_TOKEN_SIGNING_KEY. " + + "The key should be a Base64 encoded string representing at least 256 bits of data. " + + "This will require to generate new tokens for all UI users and scripts that use JWT. " + + "To keep the default non-secure JWT signing key set TB_ALLOW_DEFAULT_JWT_SIGNING_KEY=true and restart the upgrade. " + + "You may change the JWT signing key later in the Admin Settings UI."; log.error(message); throw new ValidationException(message); } diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java index 4e91c654e0..c6ec252d9f 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java @@ -16,6 +16,7 @@ package org.thingsboard.server.config.jwt; import lombok.AllArgsConstructor; +import org.apache.commons.lang3.RandomUtils; import org.apache.commons.lang3.StringUtils; import org.bouncycastle.util.Arrays; import org.springframework.stereotype.Component; @@ -23,6 +24,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import java.util.Base64; import java.util.Optional; +import java.util.concurrent.TimeUnit; @Component @AllArgsConstructor @@ -32,11 +34,14 @@ public class JwtSettingsValidator { if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) { throw new DataValidationException("JWT token issuer should be specified!"); } - if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) <= 0) { - throw new DataValidationException("JWT refresh token expiration time should be specified!"); + if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(15)) { + throw new DataValidationException("JWT refresh token expiration time should be at least 15 minutes!"); } - if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= 0) { - throw new DataValidationException("JWT token expiration time should be specified!"); + if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(1)) { + throw new DataValidationException("JWT token expiration time should be at least 1 minute!"); + } + if (jwtSettings.getTokenExpirationTime() >= jwtSettings.getRefreshTokenExpTime()) { + throw new DataValidationException("JWT token expiration time should greater than JWT refresh token expiration time!"); } if (StringUtils.isEmpty(jwtSettings.getTokenSigningKey())) { throw new DataValidationException("JWT token signing key should be specified!"); @@ -52,7 +57,11 @@ public class JwtSettingsValidator { if (Arrays.isNullOrEmpty(decodedKey)) { throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!"); } - Arrays.fill(decodedKey, (byte) 0); + if (decodedKey.length * Byte.SIZE < 256) { + throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 256 bits of data!"); + } + + System.arraycopy(decodedKey, 0, RandomUtils.nextBytes(decodedKey.length), 0, decodedKey.length); //secure memory } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index df7f0922ea..8d3ec4b55a 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -178,6 +178,7 @@ public abstract class AbstractConsumerService Date: Thu, 10 Nov 2022 19:06:21 +0100 Subject: [PATCH 098/226] jwt settings validation except install, warn on each reload if token key is default --- .../server/config/jwt/JwtSettingsService.java | 2 - .../config/jwt/JwtSettingsServiceDefault.java | 47 ++++--------- .../config/jwt/JwtSettingsValidator.java | 51 +------------- .../jwt/JwtSettingsValidatorDefault.java | 69 +++++++++++++++++++ .../jwt/JwtSettingsValidatorInstall.java} | 23 +++---- .../install/ThingsboardInstallService.java | 6 -- .../ConditionValidatorUpgradeService.java | 22 ------ 7 files changed, 94 insertions(+), 126 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java rename application/src/main/java/org/thingsboard/server/{service/install/ConditionValidatorUpgradeServiceImpl.java => config/jwt/JwtSettingsValidatorInstall.java} (58%) delete mode 100644 application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeService.java diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java index 252b0a021c..bb0127a39e 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java @@ -25,6 +25,4 @@ public interface JwtSettingsService { JwtSettings saveJwtSettings(JwtSettings jwtSettings); - void validateJwtTokenSigningKey(); - } diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java index f64b7c4658..82657c5952 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java @@ -32,7 +32,6 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.dao.settings.AdminSettingsService; import javax.annotation.PostConstruct; -import javax.validation.ValidationException; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Objects; @@ -84,8 +83,10 @@ public class JwtSettingsServiceDefault implements JwtSettingsService { jwtSettings.setTokenSigningKey(jwtLoaded.getTokenSigningKey()); } - if (hasDefaultTokenSigningKey() && !isFirstInstall()) { - log.warn("JWT token signing key is default. This is a security issue. Please, consider to set unique value"); + if (hasDefaultTokenSigningKey()) { + log.warn("WARNING: The platform is configured to use default JWT Signing Key. " + + "This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " + + "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator."); } } @@ -107,17 +108,18 @@ public class JwtSettingsServiceDefault implements JwtSettingsService { return TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey()); } + /** + * Create JWT admin settings is intended to be called from Install or Upgrade scripts + * */ @Override public void createJwtAdminSettings() { - log.debug("Creating JWT admin settings..."); + log.info("Creating JWT admin settings..."); Objects.requireNonNull(jwtSettings, "JWT settings is null"); if (isJwtAdminSettingsNotExists()) { - if (hasDefaultTokenSigningKey()) { - if (!isAllowedDefaultJwtSigningKey()) { - log.info("JWT token signing key is default. Generating a new random key"); - jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( - RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); - } + if (hasDefaultTokenSigningKey() && isFirstInstall()) { + log.info("JWT token signing key is default. Generating a new random key"); + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( + RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); } saveJwtSettings(jwtSettings); } @@ -150,29 +152,4 @@ public class JwtSettingsServiceDefault implements JwtSettingsService { return adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); } - /* - * Allowing default JWT signing key is not secure - * */ - boolean isAllowedDefaultJwtSigningKey() { - String allowDefaultJwtSigningKey = System.getenv(TB_ALLOW_DEFAULT_JWT_SIGNING_KEY); - return "true".equalsIgnoreCase(allowDefaultJwtSigningKey); - } - - @Override - public void validateJwtTokenSigningKey() { - if (isJwtAdminSettingsNotExists() && hasDefaultTokenSigningKey()) { - if (isAllowedDefaultJwtSigningKey()) { - log.warn("Default JWT signing key is allowed. This is a security issue. Please, consider to set a strong key in admin settings"); - } else { - String message = "UPGRADE ERROR. YOUR ACTION REQUIRED. Please, set a unique signing key with env variable JWT_TOKEN_SIGNING_KEY. " + - "The key should be a Base64 encoded string representing at least 256 bits of data. " + - "This will require to generate new tokens for all UI users and scripts that use JWT. " + - "To keep the default non-secure JWT signing key set TB_ALLOW_DEFAULT_JWT_SIGNING_KEY=true and restart the upgrade. " + - "You may change the JWT signing key later in the Admin Settings UI."; - log.error(message); - throw new ValidationException(message); - } - } - } - } diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java index c6ec252d9f..8e0d4afe7a 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java @@ -15,53 +15,6 @@ */ package org.thingsboard.server.config.jwt; -import lombok.AllArgsConstructor; -import org.apache.commons.lang3.RandomUtils; -import org.apache.commons.lang3.StringUtils; -import org.bouncycastle.util.Arrays; -import org.springframework.stereotype.Component; -import org.thingsboard.server.dao.exception.DataValidationException; - -import java.util.Base64; -import java.util.Optional; -import java.util.concurrent.TimeUnit; - -@Component -@AllArgsConstructor -public class JwtSettingsValidator { - - public void validate(JwtSettings jwtSettings) { - if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) { - throw new DataValidationException("JWT token issuer should be specified!"); - } - if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(15)) { - throw new DataValidationException("JWT refresh token expiration time should be at least 15 minutes!"); - } - if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(1)) { - throw new DataValidationException("JWT token expiration time should be at least 1 minute!"); - } - if (jwtSettings.getTokenExpirationTime() >= jwtSettings.getRefreshTokenExpTime()) { - throw new DataValidationException("JWT token expiration time should greater than JWT refresh token expiration time!"); - } - if (StringUtils.isEmpty(jwtSettings.getTokenSigningKey())) { - throw new DataValidationException("JWT token signing key should be specified!"); - } - - byte[] decodedKey; - try { - decodedKey = Base64.getDecoder().decode(jwtSettings.getTokenSigningKey()); - } catch (Exception e) { - throw new DataValidationException("JWT token signing key should be valid Base64 encoded string! " + e.getCause()); - } - - if (Arrays.isNullOrEmpty(decodedKey)) { - throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!"); - } - if (decodedKey.length * Byte.SIZE < 256) { - throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 256 bits of data!"); - } - - System.arraycopy(decodedKey, 0, RandomUtils.nextBytes(decodedKey.length), 0, decodedKey.length); //secure memory - } - +public interface JwtSettingsValidator { + void validate(JwtSettings jwtSettings); } diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java new file mode 100644 index 0000000000..ee60a80de6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java @@ -0,0 +1,69 @@ +/** + * Copyright © 2016-2022 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.config.jwt; + +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.RandomUtils; +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.util.Arrays; +import org.springframework.stereotype.Component; +import org.thingsboard.server.dao.exception.DataValidationException; + +import java.util.Base64; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +@Component +@RequiredArgsConstructor +public class JwtSettingsValidatorDefault implements JwtSettingsValidator { + + @Override + public void validate(JwtSettings jwtSettings) { + if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) { + throw new DataValidationException("JWT token issuer should be specified!"); + } + if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(15)) { + throw new DataValidationException("JWT refresh token expiration time should be at least 15 minutes!"); + } + if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(1)) { + throw new DataValidationException("JWT token expiration time should be at least 1 minute!"); + } + if (jwtSettings.getTokenExpirationTime() >= jwtSettings.getRefreshTokenExpTime()) { + throw new DataValidationException("JWT token expiration time should greater than JWT refresh token expiration time!"); + } + if (StringUtils.isEmpty(jwtSettings.getTokenSigningKey())) { + throw new DataValidationException("JWT token signing key should be specified!"); + } + + byte[] decodedKey; + try { + decodedKey = Base64.getDecoder().decode(jwtSettings.getTokenSigningKey()); + } catch (Exception e) { + throw new DataValidationException("JWT token signing key should be valid Base64 encoded string! " + e.getCause()); + } + + if (Arrays.isNullOrEmpty(decodedKey)) { + throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!"); + } + if (decodedKey.length * Byte.SIZE < 256) { + throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 256 bits of data!"); + } + + System.arraycopy(decodedKey, 0, RandomUtils.nextBytes(decodedKey.length), 0, decodedKey.length); //secure memory + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorInstall.java similarity index 58% rename from application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java rename to application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorInstall.java index 8dbfaab893..e353eb57a2 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorInstall.java @@ -13,26 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.install; +package org.thingsboard.server.config.jwt; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; -import org.thingsboard.server.config.jwt.JwtSettingsService; +import org.springframework.stereotype.Component; -@Service +@Primary @Profile("install") +@Component @RequiredArgsConstructor -@Slf4j -public class ConditionValidatorUpgradeServiceImpl implements ConditionValidatorUpgradeService { - - private final JwtSettingsService jwtSettingsService; +public class JwtSettingsValidatorInstall implements JwtSettingsValidator { + /** + * During Install or upgrade the validation is suppressed to keep existing data + * */ @Override - public void validateConditionsBeforeUpgrade(String fromVersion) throws Exception { - log.info("Validating conditions before upgrade..."); - jwtSettingsService.validateJwtTokenSigningKey(); + public void validate(JwtSettings jwtSettings) { + } } diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index a100c61035..277e0b4d38 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -34,7 +34,6 @@ import org.thingsboard.server.service.install.migrate.EntitiesMigrateService; import org.thingsboard.server.service.install.migrate.TsLatestMigrateService; import org.thingsboard.server.service.install.update.CacheCleanupService; import org.thingsboard.server.service.install.update.DataUpdateService; -import org.thingsboard.server.service.install.ConditionValidatorUpgradeService; @Service @Profile("install") @@ -89,16 +88,11 @@ public class ThingsboardInstallService { @Autowired(required = false) private TsLatestMigrateService latestMigrateService; - @Autowired - private ConditionValidatorUpgradeService conditionValidatorUpgradeService; - public void performInstall() { try { if (isUpgrade) { log.info("Starting ThingsBoard Upgrade from version {} ...", upgradeFromVersion); - conditionValidatorUpgradeService.validateConditionsBeforeUpgrade(upgradeFromVersion); - cacheCleanupService.clearCache(upgradeFromVersion); if ("2.5.0-cassandra".equals(upgradeFromVersion)) { diff --git a/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeService.java deleted file mode 100644 index ec98a2f37e..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeService.java +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright © 2016-2022 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.install; - -public interface ConditionValidatorUpgradeService { - - void validateConditionsBeforeUpgrade(String fromVersion) throws Exception; - -} From b776cf13b60014b5363260a08614ac61117376c6 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 10 Nov 2022 23:48:08 +0100 Subject: [PATCH 099/226] jwt settings code cleanup --- .../server/config/jwt/JwtSettingsServiceDefault.java | 1 - application/src/main/resources/thingsboard.yml | 2 +- lombok.config | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java index 82657c5952..73867b1334 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java @@ -44,7 +44,6 @@ public class JwtSettingsServiceDefault implements JwtSettingsService { static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; static final String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; - static final String TB_ALLOW_DEFAULT_JWT_SIGNING_KEY = "TB_ALLOW_DEFAULT_JWT_SIGNING_KEY"; @Lazy private final AdminSettingsService adminSettingsService; @Lazy diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 9d3eaf8f4a..ac6bed5b0f 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -107,7 +107,7 @@ plugins: # Security parameters security: # JWT Token parameters - jwt: # Since 3.5.0 values are persisted to the database during install or upgrade. On Install, the key will be generated randomly if no custom value set. + jwt: # Since 3.4.2 values are persisted to the database during install or upgrade. On Install, the key will be generated randomly if no custom value set. You can change it later from Web UI under SYS_ADMIN tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}" # Number of seconds (2.5 hours) refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week). tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}" diff --git a/lombok.config b/lombok.config index 2299612bb9..1b8f891cd9 100644 --- a/lombok.config +++ b/lombok.config @@ -1,3 +1,3 @@ config.stopbubbling = true lombok.anyconstructor.addconstructorproperties = true -lombok.copyableAnnotations += org.springframework.context.annotation.Lazy \ No newline at end of file +lombok.copyableAnnotations += org.springframework.context.annotation.Lazy From a00d68f477cd6390f2010c6d350b99425a778c5a Mon Sep 17 00:00:00 2001 From: Yuriy Lytvynchuk Date: Fri, 11 Nov 2022 10:06:02 +0200 Subject: [PATCH 100/226] delete cases: TENANT, DASHBOARD, CUSTOMER --- .../util/EntitiesByNameAndTypeLoader.java | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java index d70865f197..97bf05b2b9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java @@ -45,15 +45,6 @@ public class EntitiesByNameAndTypeLoader { targetEntityId = asset.getId(); } break; - case CUSTOMER: - Optional customerOptional = ctx.getCustomerService().findCustomerByTenantIdAndTitle(ctx.getTenantId(), entityName); - if (customerOptional.isPresent()) { - targetEntityId = customerOptional.get().getId(); - } - break; - case TENANT: - targetEntityId = ctx.getTenantId(); - break; case ENTITY_VIEW: EntityView entityView = ctx.getEntityViewService().findEntityViewByTenantIdAndName(ctx.getTenantId(), entityName); if (entityView != null) { @@ -66,14 +57,8 @@ public class EntitiesByNameAndTypeLoader { targetEntityId = edge.getId(); } break; - case DASHBOARD: - DashboardInfo dashboardInfo = ctx.getDashboardService().findFirstDashboardInfoByTenantIdAndName(ctx.getTenantId(), entityName); - if (dashboardInfo != null) { - targetEntityId = dashboardInfo.getId(); - } - break; case USER: - User user = ctx.getUserService().findUserByEmail(ctx.getTenantId(), entityName); + User user = ctx.getUserService().findUserByTenantIdAndEmail(ctx.getTenantId(), entityName); if (user != null) { targetEntityId = user.getId(); } From b1d5e89e9c76d042a00f5a6c43e48931e288453c Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 11 Nov 2022 10:51:25 +0200 Subject: [PATCH 101/226] refactoring --- .../server/msa/AbstractContainerTest.java | 4 +- .../server/msa/ContainerTestSuite.java | 2 +- .../server/msa/TestProperties.java | 43 +++++++++++-------- .../server/msa/TestRestClient.java | 15 +++---- .../msa/connectivity/HttpClientTest.java | 4 +- .../msa/connectivity/MqttClientTest.java | 1 - .../connectivity/MqttGatewayClientTest.java | 12 ++++-- 7 files changed, 44 insertions(+), 37 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java index e9e6036771..d647a6d927 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java @@ -29,8 +29,10 @@ import org.testng.annotations.BeforeSuite; import org.testng.annotations.Listeners; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; + import java.net.URI; -import java.util.*; +import java.util.Map; +import java.util.Random; @Slf4j diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java index c32a6bdbe6..2ff7088fa0 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java @@ -54,7 +54,7 @@ public class ContainerTestSuite { private DockerComposeContainer testContainer; private ThingsBoardDbInstaller installTb; - public boolean isActive; + private boolean isActive; private static ContainerTestSuite containerTestSuite; diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java index 6e3a024cda..9acc2c2046 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java @@ -15,44 +15,49 @@ */ package org.thingsboard.server.msa; -import java.io.FileInputStream; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + import java.io.IOException; import java.io.InputStream; import java.util.Properties; +@Slf4j +@Component public class TestProperties { - public static final String HTTPS_URL = "https://localhost"; - protected static final String WSS_URL = "wss://localhost"; + private static final String HTTPS_URL = "https://localhost"; + + private static final String WSS_URL = "wss://localhost"; + + private static final ContainerTestSuite instance = ContainerTestSuite.getInstance(); - public static final ContainerTestSuite instance = ContainerTestSuite.getInstance(); + private static Properties properties; - public static String getBaseUrl(){ + public static String getBaseUrl() { if (instance.isActive()) { return HTTPS_URL; } - return getProperty("tb.baseUrl"); + return getProperties().getProperty("tb.baseUrl"); } - public static String getWebSocketUrl(){ + public static String getWebSocketUrl() { if (instance.isActive()) { return WSS_URL; } - return getProperty("tb.wsUrl"); + return getProperties().getProperty("tb.wsUrl"); } - private static String getProperty(String propertyName) { - - try (InputStream input = TestProperties.class.getClassLoader().getResourceAsStream("config.properties")) { - Properties prop = new Properties(); - prop.load(input); - return prop.getProperty(propertyName); - - } catch (IOException ex) { - ex.printStackTrace(); + private static Properties getProperties() { + if (properties == null) { + try (InputStream input = TestProperties.class.getClassLoader().getResourceAsStream("config.properties")) { + properties = new Properties(); + properties.load(input); + } catch (IOException ex) { + log.error("Exception while reading test properties " + ex.getMessage()); + } } - return null; - + return properties; } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index 0b0c4b89bc..b1907a0d99 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -17,10 +17,8 @@ package org.thingsboard.server.msa; import com.fasterxml.jackson.databind.JsonNode; import io.restassured.RestAssured; -import io.restassured.builder.ResponseSpecBuilder; import io.restassured.common.mapper.TypeRef; import io.restassured.config.HeaderConfig; -import io.restassured.config.HttpClientConfig; import io.restassured.config.RestAssuredConfig; import io.restassured.filter.log.RequestLoggingFilter; import io.restassured.filter.log.ResponseLoggingFilter; @@ -28,9 +26,10 @@ import io.restassured.http.ContentType; import io.restassured.path.json.JsonPath; import io.restassured.response.ValidatableResponse; import io.restassured.specification.RequestSpecification; -import io.restassured.specification.ResponseSpecification; -import org.hamcrest.Matchers; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; @@ -48,8 +47,6 @@ import java.util.List; import java.util.Map; import static io.restassured.RestAssured.given; -import static org.apache.http.params.CoreConnectionPNames.CONNECTION_TIMEOUT; -import static org.apache.http.params.CoreConnectionPNames.SO_TIMEOUT; import static org.hamcrest.Matchers.is; import static org.hamcrest.core.AnyOf.anyOf; import static org.thingsboard.server.common.data.StringUtils.isEmpty; @@ -59,9 +56,7 @@ public class TestRestClient { private final String baseURL; private String token; private String refreshToken; - private RequestSpecification requestSpec; - private ResponseSpecification responseSpec; - protected static final String ACTIVATE_TOKEN_REGEX = "/api/noauth/activate?activateToken="; + private final RequestSpecification requestSpec; public TestRestClient(String url) { RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); @@ -78,7 +73,7 @@ public class TestRestClient { } } - public void login(String username, String password) throws Exception { + public void login(String username, String password) { Map loginRequest = new HashMap<>(); loginRequest.put("username", username); loginRequest.put("password", password); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java index 6f24f00854..2eb4cdf076 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java @@ -25,12 +25,12 @@ import org.thingsboard.server.msa.AbstractContainerTest; import org.thingsboard.server.msa.WsClient; import org.thingsboard.server.msa.mapper.WsTelemetryResponse; - import java.util.Arrays; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; -import static org.thingsboard.server.common.data.DataConstants.*; +import static org.thingsboard.server.common.data.DataConstants.DEVICE; +import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; public class HttpClientTest extends AbstractContainerTest { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java index 7e7ebf07ab..55dd432644 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java @@ -64,7 +64,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.testng.Assert.fail; import static org.thingsboard.server.common.data.DataConstants.DEVICE; import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; -import static org.thingsboard.server.msa.TestProperties.HTTPS_URL; import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; @Slf4j diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java index ab42abc88e..c97214e18c 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java @@ -28,7 +28,6 @@ import io.netty.handler.codec.mqtt.MqttQoS; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; @@ -52,8 +51,15 @@ import org.thingsboard.server.msa.mapper.WsTelemetryResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.concurrent.*; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.common.data.DataConstants.DEVICE; From 62ca7447c44e8d239be4c76ff8fc3591bd503ffa Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 11 Nov 2022 11:06:36 +0200 Subject: [PATCH 102/226] refactoring --- .../server/msa/TestProperties.java | 2 - .../server/msa/TestRestClient.java | 40 +++++++++---------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java index 9acc2c2046..020dbf8b93 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java @@ -16,14 +16,12 @@ package org.thingsboard.server.msa; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; import java.io.IOException; import java.io.InputStream; import java.util.Properties; @Slf4j -@Component public class TestProperties { private static final String HTTPS_URL = "https://localhost"; diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index b1907a0d99..a83913f842 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -26,10 +26,6 @@ import io.restassured.http.ContentType; import io.restassured.path.json.JsonPath; import io.restassured.response.ValidatableResponse; import io.restassured.specification.RequestSpecification; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; @@ -47,6 +43,8 @@ import java.util.List; import java.util.Map; import static io.restassured.RestAssured.given; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static java.net.HttpURLConnection.HTTP_OK; import static org.hamcrest.Matchers.is; import static org.hamcrest.core.AnyOf.anyOf; import static org.thingsboard.server.common.data.StringUtils.isEmpty; @@ -54,9 +52,9 @@ import static org.thingsboard.server.common.data.StringUtils.isEmpty; public class TestRestClient { private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization"; private final String baseURL; + private final RequestSpecification requestSpec; private String token; private String refreshToken; - private final RequestSpecification requestSpec; public TestRestClient(String url) { RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); @@ -90,7 +88,7 @@ public class TestRestClient { .pathParams("accessToken", accessToken) .post("/api/device?accessToken={accessToken}") .then() - .statusCode(HttpStatus.OK.value()) + .statusCode(HTTP_OK) .extract() .as(Device.class); } @@ -103,7 +101,7 @@ public class TestRestClient { .statusCode(statusCode); } public Device getDeviceById(DeviceId deviceId) { - return getDeviceById(deviceId, HttpStatus.OK.value()) + return getDeviceById(deviceId, HTTP_OK) .extract() .as(Device.class); } @@ -111,7 +109,7 @@ public class TestRestClient { return given().spec(requestSpec).get("/api/device/{deviceId}/credentials", deviceId.getId()) .then() .assertThat() - .statusCode(HttpStatus.OK.value()) + .statusCode(HTTP_OK) .extract() .as(DeviceCredentials.class); } @@ -120,34 +118,34 @@ public class TestRestClient { return given().spec(requestSpec).body(telemetry) .post("/api/v1/{credentialsId}/telemetry", credentialsId) .then() - .statusCode(HttpStatus.OK.value()); + .statusCode(HTTP_OK); } public ValidatableResponse deleteDevice(DeviceId deviceId) { return given().spec(requestSpec) .delete("/api/device/{deviceId}", deviceId.getId()) .then() - .statusCode(HttpStatus.OK.value()); + .statusCode(HTTP_OK); } public ValidatableResponse deleteDeviceIfExists(DeviceId deviceId) { return given().spec(requestSpec) .delete("/api/device/{deviceId}", deviceId.getId()) .then() - .statusCode(anyOf(is(HttpStatus.OK.value()),is(HttpStatus.NOT_FOUND.value()))); + .statusCode(anyOf(is(HTTP_OK),is(HTTP_NOT_FOUND))); } public ValidatableResponse postTelemetryAttribute(String entityType, DeviceId deviceId, String scope, JsonNode attribute) { return given().spec(requestSpec).body(attribute) .post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityType, deviceId.getId(), scope) .then() - .statusCode(HttpStatus.OK.value()); + .statusCode(HTTP_OK); } public ValidatableResponse postAttribute(String accessToken, JsonNode attribute) { return given().spec(requestSpec).body(attribute) .post("/api/v1/{accessToken}/attributes/", accessToken) .then() - .statusCode(HttpStatus.OK.value()); + .statusCode(HTTP_OK); } public JsonNode getAttributes(String accessToken, String clientKeys, String sharedKeys) { @@ -156,7 +154,7 @@ public class TestRestClient { .queryParam("sharedKeys", sharedKeys) .get("/api/v1/{accessToken}/attributes", accessToken) .then() - .statusCode(HttpStatus.OK.value()) + .statusCode(HTTP_OK) .extract() .as(JsonNode.class); } @@ -167,7 +165,7 @@ public class TestRestClient { return given().spec(requestSpec).queryParams(params) .get("/api/ruleChains") .then() - .statusCode(HttpStatus.OK.value()) + .statusCode(HTTP_OK) .extract() .as(new TypeRef>() {}); } @@ -177,7 +175,7 @@ public class TestRestClient { .body(ruleChain) .post("/api/ruleChain") .then() - .statusCode(HttpStatus.OK.value()) + .statusCode(HTTP_OK) .extract() .as(RuleChain.class); } @@ -187,7 +185,7 @@ public class TestRestClient { .body(ruleChainMetaData) .post("/api/ruleChain/metadata") .then() - .statusCode(HttpStatus.OK.value()) + .statusCode(HTTP_OK) .extract() .as(RuleChainMetaData.class); } @@ -196,14 +194,14 @@ public class TestRestClient { given().spec(requestSpec) .post("/api/ruleChain/{ruleChainId}/root", ruleChainId.getId()) .then() - .statusCode(HttpStatus.OK.value()); + .statusCode(HTTP_OK); } public void deleteRuleChain(RuleChainId ruleChainId) { given().spec(requestSpec) .delete("/api/ruleChain/{ruleChainId}", ruleChainId.getId()) .then() - .statusCode(HttpStatus.OK.value()); + .statusCode(HTTP_OK); } private String getUrlParams(PageLink pageLink) { @@ -239,7 +237,7 @@ public class TestRestClient { .pathParams(params) .get("/api/relations?fromId={fromId}&fromType={fromType}&relationTypeGroup={relationTypeGroup}") .then() - .statusCode(HttpStatus.OK.value()) + .statusCode(HTTP_OK) .extract() .as(new TypeRef>() {}); } @@ -249,7 +247,7 @@ public class TestRestClient { .body(serverRpcPayload) .post("/api/rpc/twoway/{deviceId}", deviceId.getId()) .then() - .statusCode(HttpStatus.OK.value()) + .statusCode(HTTP_OK) .extract() .as(JsonNode.class); } From e11b5ffedd381f94a11f8f1e02df2cf62ad4aed6 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 11 Nov 2022 12:33:16 +0200 Subject: [PATCH 103/226] refactoring --- .../thingsboard/server/msa/AbstractContainerTest.java | 2 -- .../org/thingsboard/server/msa/TestRestClient.java | 10 +++++----- .../server/msa/connectivity/MqttGatewayClientTest.java | 1 + 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java index d647a6d927..a148d7d138 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; import org.apache.http.conn.ssl.TrustStrategy; import org.apache.http.ssl.SSLContextBuilder; @@ -40,7 +39,6 @@ import java.util.Random; public abstract class AbstractContainerTest { protected static long timeoutMultiplier = 1; protected ObjectMapper mapper = new ObjectMapper(); - protected JsonParser jsonParser = new JsonParser(); private static final ContainerTestSuite containerTestSuite = ContainerTestSuite.getInstance(); protected static TestRestClient testRestClient; diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index a83913f842..713205e228 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -51,7 +51,7 @@ import static org.thingsboard.server.common.data.StringUtils.isEmpty; public class TestRestClient { private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization"; - private final String baseURL; + private static final String CONTENT_TYPE_HEADER = "Content-Type"; private final RequestSpecification requestSpec; private String token; private String refreshToken; @@ -59,12 +59,11 @@ public class TestRestClient { public TestRestClient(String url) { RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); - baseURL = url; - requestSpec = given().baseUri(baseURL) + requestSpec = given().baseUri(url) .contentType(ContentType.JSON) .config(RestAssuredConfig.config() .headerConfig(HeaderConfig.headerConfig() - .overwriteHeadersWithName(JWT_TOKEN_HEADER_PARAM))); + .overwriteHeadersWithName(JWT_TOKEN_HEADER_PARAM, CONTENT_TYPE_HEADER))); if (url.matches("^(https)://.*$")) { requestSpec.relaxedHTTPSValidation(); @@ -76,7 +75,8 @@ public class TestRestClient { loginRequest.put("username", username); loginRequest.put("password", password); - JsonPath jsonPath = given().relaxedHTTPSValidation().body(loginRequest).post(baseURL + "/api/auth/login") + JsonPath jsonPath = given().spec(requestSpec).body(loginRequest) + .post( "/api/auth/login") .getBody().jsonPath(); token = jsonPath.get("token"); refreshToken = jsonPath.get("refreshToken"); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java index c97214e18c..84b2dd7253 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java @@ -72,6 +72,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest { private MqttClient mqttClient; private Device createdDevice; private MqttMessageListener listener; + private JsonParser jsonParser = new JsonParser(); @BeforeMethod public void createGateway() throws Exception { From f49b34939677a98cb4e11916ea1592ef3c8d9f8d Mon Sep 17 00:00:00 2001 From: Yuriy Lytvynchuk Date: Fri, 11 Nov 2022 12:50:55 +0200 Subject: [PATCH 104/226] refactor code --- .../util/EntitiesByNameAndTypeLoader.java | 41 ++++--------------- 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java index 97bf05b2b9..3c3417ad25 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java @@ -16,60 +16,37 @@ package org.thingsboard.rule.engine.util; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.DashboardInfo; -import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.EntityId; -import java.util.Optional; - public class EntitiesByNameAndTypeLoader { public static EntityId findEntityId(TbContext ctx, EntityType entityType, String entityName) { - EntityId targetEntityId = null; + SearchTextBasedWithAdditionalInfo targetEntity; switch (entityType) { case DEVICE: - Device device = ctx.getDeviceService().findDeviceByTenantIdAndName(ctx.getTenantId(), entityName); - if (device != null) { - targetEntityId = device.getId(); - } + targetEntity = ctx.getDeviceService().findDeviceByTenantIdAndName(ctx.getTenantId(), entityName); break; case ASSET: - Asset asset = ctx.getAssetService().findAssetByTenantIdAndName(ctx.getTenantId(), entityName); - if (asset != null) { - targetEntityId = asset.getId(); - } + targetEntity = ctx.getAssetService().findAssetByTenantIdAndName(ctx.getTenantId(), entityName); break; case ENTITY_VIEW: - EntityView entityView = ctx.getEntityViewService().findEntityViewByTenantIdAndName(ctx.getTenantId(), entityName); - if (entityView != null) { - targetEntityId = entityView.getId(); - } + targetEntity = ctx.getEntityViewService().findEntityViewByTenantIdAndName(ctx.getTenantId(), entityName); break; case EDGE: - Edge edge = ctx.getEdgeService().findEdgeByTenantIdAndName(ctx.getTenantId(), entityName); - if (edge != null) { - targetEntityId = edge.getId(); - } + targetEntity = ctx.getEdgeService().findEdgeByTenantIdAndName(ctx.getTenantId(), entityName); break; case USER: - User user = ctx.getUserService().findUserByTenantIdAndEmail(ctx.getTenantId(), entityName); - if (user != null) { - targetEntityId = user.getId(); - } + targetEntity = ctx.getUserService().findUserByTenantIdAndEmail(ctx.getTenantId(), entityName); break; default: throw new IllegalStateException("Unexpected entity type " + entityType.name()); } - if (targetEntityId == null) { + if (targetEntity == null) { throw new IllegalStateException("Failed to found " + entityType.name() + " entity by name: '" + entityName + "'!"); } - return targetEntityId; + return targetEntity.getId(); } } From 7b9c9e51cd165033a7654b1994daba3dc2bf2c04 Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Fri, 11 Nov 2022 14:41:15 +0200 Subject: [PATCH 105/226] update build of main query for alarm count query --- .../server/dao/sql/query/DefaultAlarmQueryRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index 66219eb9ba..d91a8c5fc4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -237,7 +237,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { String mainQuery = String.format("%s%s", selectPart, fromPart); if (textSearchQuery.isEmpty()) { - mainQuery += String.format("%s%s", joinPart, wherePart); + mainQuery = String.format("%s%s%s", mainQuery, joinPart, wherePart); } else { mainQuery = String.format("select * from (%s%s) a %s WHERE %s", mainQuery, wherePart, joinPart, textSearchQuery); } From 8a29dc294e35f130d88438736f13c71a02f9d5b8 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 11 Nov 2022 17:41:12 +0200 Subject: [PATCH 106/226] mute TelemetryEdgeSqlTest that causes a lot of randomly generated errors --- application/src/test/resources/logback-test.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index d3301bf660..3762c8aa7c 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -16,6 +16,8 @@ + + From fc1da1299969dacf4f3d280f81905fd58ea751bd Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 14 Nov 2022 11:05:47 +0200 Subject: [PATCH 107/226] UI: Add JWT security settings form and restyle security page --- ui-ngx/src/app/core/http/admin.service.ts | 18 +- .../admin/security-settings.component.html | 373 +++++++++++------- .../admin/security-settings.component.scss | 23 +- .../admin/security-settings.component.ts | 132 ++++++- .../dialog/confirm-dialog.component.html | 2 +- .../src/app/shared/models/settings.models.ts | 7 + .../assets/locale/locale.constant-en_US.json | 20 + 7 files changed, 410 insertions(+), 165 deletions(-) diff --git a/ui-ngx/src/app/core/http/admin.service.ts b/ui-ngx/src/app/core/http/admin.service.ts index 485f355b87..8933f7b99d 100644 --- a/ui-ngx/src/app/core/http/admin.service.ts +++ b/ui-ngx/src/app/core/http/admin.service.ts @@ -20,16 +20,18 @@ import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { AdminSettings, - RepositorySettings, + AutoCommitSettings, + JwtSettings, MailServerSettings, + RepositorySettings, + RepositorySettingsInfo, SecuritySettings, TestSmsRequest, - UpdateMessage, - AutoCommitSettings, - RepositorySettingsInfo + UpdateMessage } from '@shared/models/settings.models'; import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; import { tap } from 'rxjs/operators'; +import { LoginResponse } from '@shared/models/login.models'; @Injectable({ providedIn: 'root' @@ -70,6 +72,14 @@ export class AdminService { defaultHttpOptionsFromConfig(config)); } + public getJwtSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/admin/jwtSettings`, defaultHttpOptionsFromConfig(config)); + } + + public saveJwtSettings(jwtSettings: JwtSettings, config?: RequestConfig): Observable { + return this.http.post('/api/admin/jwtSettings', jwtSettings, defaultHttpOptionsFromConfig(config)); + } + public getRepositorySettings(config?: RequestConfig): Observable { return this.http.get(`/api/admin/repositorySettings`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html index 80c9f384fd..7a498a0f20 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html @@ -15,144 +15,237 @@ limitations under the License. --> -
- - -
- admin.security-settings - -
-
-
- - -
- -
-
-
- - - - -
admin.general-policy
-
-
- - admin.max-failed-login-attempts - - - {{ 'admin.minimum-max-failed-login-attempts-range' | translate }} - - - - admin.user-lockout-notification-email - - -
- - - -
admin.password-policy
-
-
-
- - admin.minimum-password-length - - - {{ 'admin.minimum-password-length-required' | translate }} - - - {{ 'admin.minimum-password-length-range' | translate }} - - - {{ 'admin.minimum-password-length-range' | translate }} - - - - admin.minimum-uppercase-letters - - - {{ 'admin.minimum-uppercase-letters-range' | translate }} - - - - admin.minimum-lowercase-letters - - - {{ 'admin.minimum-lowercase-letters-range' | translate }} - - - - admin.minimum-digits - - - {{ 'admin.minimum-digits-range' | translate }} - - - - admin.minimum-special-characters - - - {{ 'admin.minimum-special-characters-range' | translate }} - - - - admin.password-expiration-period-days - - - {{ 'admin.password-expiration-period-days-range' | translate }} - - - - admin.password-reuse-frequency-days - - - {{ 'admin.password-reuse-frequency-days-range' | translate }} - - - - admin.allow-whitespace - -
-
-
-
-
- -
+ + +
+ admin.security-settings + +
+
+
+ + +
+ + +
+
+ admin.general-policy + + admin.max-failed-login-attempts + + + {{ 'admin.minimum-max-failed-login-attempts-range' | translate }} + + + + admin.user-lockout-notification-email + + +
+ +
+ admin.password-policy +
+ + admin.minimum-password-length + + + {{ 'admin.minimum-password-length-required' | translate }} + + + {{ 'admin.minimum-password-length-range' | translate }} + + + {{ 'admin.minimum-password-length-range' | translate }} + + +
+ + admin.minimum-uppercase-letters + + + {{ 'admin.minimum-uppercase-letters-range' | translate }} + + + + admin.minimum-lowercase-letters + + + {{ 'admin.minimum-lowercase-letters-range' | translate }} + + +
+
+ + admin.minimum-digits + + + {{ 'admin.minimum-digits-range' | translate }} + + + + admin.minimum-special-characters + + + {{ 'admin.minimum-special-characters-range' | translate }} + + +
+
+ + admin.password-expiration-period-days + + + {{ 'admin.password-expiration-period-days-range' | translate }} + + + + admin.password-reuse-frequency-days + + + {{ 'admin.password-reuse-frequency-days-range' | translate }} + + +
+ + admin.allow-whitespace + +
- - - -
+
+ + +
+ + + + + + +
+ admin.jwt.security-settings +
+
+ + +
+ +
+
+
+ + admin.jwt.issuer-name + + + {{ 'admin.jwt.issuer-name-required' | translate }} + + + + admin.jwt.signings-key + + + + {{ 'admin.jwt.signings-key-required' | translate }} + + + {{ 'admin.jwt.signings-key-base64' | translate }} + + +
+
+ + admin.jwt.expiration-time + + + {{ 'admin.jwt.expiration-time-required' | translate }} + + + {{ 'admin.jwt.expiration-time-pattern' | translate }} + + + {{ 'admin.jwt.expiration-time-min' | translate }} + + + + admin.jwt.refresh-expiration-time + + + {{ 'admin.jwt.refresh-expiration-time-required' | translate }} + + + {{ 'admin.jwt.refresh-expiration-time-pattern' | translate }} + + + {{ 'admin.jwt.refresh-expiration-time-min' | translate }} + + + {{ 'admin.jwt.refresh-expiration-time-less-token' | translate }} + + +
+
+ + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.scss index 32e514e010..5a6d5eed04 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.scss +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.scss @@ -14,7 +14,26 @@ * limitations under the License. */ :host { - .mat-accordion-container { - margin-bottom: 16px; + .mat-headline { + margin-bottom: 8px; + } + + .mat-card-title { + margin: 0; + } + + .mat-card-content { + padding: 0 !important; + } + + .fields-group { + padding: 8px 16px 0; + margin: 10px 0; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + + legend { + color: rgba(0, 0, 0, .7); + } } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts index 6dc070b278..bdf645a9d7 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts @@ -14,40 +14,50 @@ /// limitations under the License. /// -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { PageComponent } from '@shared/components/page.component'; import { Router } from '@angular/router'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { SecuritySettings } from '@shared/models/settings.models'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { JwtSettings, SecuritySettings } from '@shared/models/settings.models'; import { AdminService } from '@core/http/admin.service'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; +import { mergeMap, tap } from 'rxjs/operators'; +import { randomAlphanumeric } from '@core/utils'; +import { AuthService } from '@core/auth/auth.service'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable, of } from 'rxjs'; @Component({ selector: 'tb-security-settings', templateUrl: './security-settings.component.html', styleUrls: ['./security-settings.component.scss', './settings-card.scss'] }) -export class SecuritySettingsComponent extends PageComponent implements OnInit, HasConfirmForm { +export class SecuritySettingsComponent extends PageComponent implements HasConfirmForm { securitySettingsFormGroup: FormGroup; - securitySettings: SecuritySettings; + jwtSecuritySettingsFormGroup: FormGroup; + + private securitySettings: SecuritySettings; + private jwtSettings: JwtSettings; constructor(protected store: Store, private router: Router, private adminService: AdminService, - public fb: FormBuilder) { + private authService: AuthService, + private dialogService: DialogService, + private translate: TranslateService, + private fb: FormBuilder) { super(store); - } - - ngOnInit() { this.buildSecuritySettingsForm(); + this.buildJwtSecuritySettingsForm(); this.adminService.getSecuritySettings().subscribe( - (securitySettings) => { - this.securitySettings = securitySettings; - this.securitySettingsFormGroup.reset(this.securitySettings); - } + securitySettings => this.processSecuritySettings(securitySettings) + ); + this.adminService.getJwtSettings().subscribe( + jwtSettings => this.processJwtSettings(jwtSettings) ); } @@ -70,18 +80,104 @@ export class SecuritySettingsComponent extends PageComponent implements OnInit, }); } + buildJwtSecuritySettingsForm() { + this.jwtSecuritySettingsFormGroup = this.fb.group({ + tokenIssuer: ['', Validators.required], + tokenSigningKey: ['', [Validators.required, this.base64Format]], + tokenExpirationTime: [0, [Validators.required, Validators.pattern('[0-9]*'), Validators.min(60)]], + refreshTokenExpTime: [0, [Validators.required, Validators.pattern('[0-9]*'), Validators.min(900)]] + }, {validators: this.refreshTokenTimeGreatTokenTime.bind(this)}); + this.jwtSecuritySettingsFormGroup.get('tokenExpirationTime').valueChanges.subscribe( + () => this.jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').updateValueAndValidity({onlySelf: true}) + ); + } + save(): void { this.securitySettings = {...this.securitySettings, ...this.securitySettingsFormGroup.value}; this.adminService.saveSecuritySettings(this.securitySettings).subscribe( - (securitySettings) => { - this.securitySettings = securitySettings; - this.securitySettingsFormGroup.reset(this.securitySettings); - } + securitySettings => this.processSecuritySettings(securitySettings) ); } + saveJwtSettings() { + const jwtFormSettings = this.jwtSecuritySettingsFormGroup.value; + this.confirmChangeJWTSettings().pipe(mergeMap(value => { + if (value) { + return this.adminService.saveJwtSettings(jwtFormSettings).pipe( + tap((data) => this.authService.setUserFromJwtToken(data.token, data.refreshToken, false)), + mergeMap(() => this.adminService.getJwtSettings()), + tap(jwtSettings => this.processJwtSettings(jwtSettings)) + ); + } + return of(null); + })).subscribe(() => {}); + } + + discardSetting() { + this.securitySettingsFormGroup.reset(this.securitySettings); + } + + discardJwtSetting() { + this.jwtSecuritySettingsFormGroup.reset(this.jwtSettings); + } + + private confirmChangeJWTSettings(): Observable { + if (this.jwtSecuritySettingsFormGroup.get('tokenIssuer').value !== (this.jwtSettings?.tokenIssuer || '') || + this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').value !== (this.jwtSettings?.tokenSigningKey || '')) { + return this.dialogService.confirm( + this.translate.instant('admin.jwt.info-header'), + `
${this.translate.instant('admin.jwt.info-message')}
`, + this.translate.instant('action.discard-changes'), + this.translate.instant('action.confirm') + ); + } + return of(true); + } + + generateSigningKey() { + this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').setValue(randomAlphanumeric(44)); + if (this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').pristine) { + this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').markAsDirty(); + this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').markAsTouched(); + } + } + + private processSecuritySettings(securitySettings: SecuritySettings) { + this.securitySettings = securitySettings; + this.securitySettingsFormGroup.reset(this.securitySettings); + } + + private processJwtSettings(jwtSettings: JwtSettings) { + this.jwtSettings = jwtSettings; + this.jwtSecuritySettingsFormGroup.reset(jwtSettings); + } + + private refreshTokenTimeGreatTokenTime(formGroup: FormGroup): { [key: string]: boolean } | null { + if (formGroup) { + const tokenTime = formGroup.value.tokenExpirationTime; + const refreshTokenTime = formGroup.value.refreshTokenExpTime; + if (tokenTime >= refreshTokenTime ) { + if (formGroup.get('refreshTokenExpTime').untouched) { + formGroup.get('refreshTokenExpTime').markAsTouched(); + } + formGroup.get('refreshTokenExpTime').setErrors({lessToken: true}); + return {lessToken: true}; + } + } + return null; + } + + private base64Format(control: FormControl): { [key: string]: boolean } | null { + try { + const value = btoa(control.value); + return null; + } catch (e) { + return {base64: true}; + } + } + confirmForm(): FormGroup { - return this.securitySettingsFormGroup; + return this.securitySettingsFormGroup.dirty ? this.securitySettingsFormGroup : this.jwtSecuritySettingsFormGroup; } } diff --git a/ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.html index 710339908b..f7586d5656 100644 --- a/ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.html @@ -16,7 +16,7 @@ -->

{{data.title}}

-
+
diff --git a/ui-ngx/src/app/shared/models/settings.models.ts b/ui-ngx/src/app/shared/models/settings.models.ts index 83426c8b3b..8ecb4066f1 100644 --- a/ui-ngx/src/app/shared/models/settings.models.ts +++ b/ui-ngx/src/app/shared/models/settings.models.ts @@ -63,6 +63,13 @@ export interface SecuritySettings { passwordPolicy: UserPasswordPolicy; } +export interface JwtSettings { + tokenIssuer: string; + tokenSigningKey: string; + tokenExpirationTime: number; + refreshTokenExpTime: number; +} + export interface UpdateMessage { message: string; updateAvailable: boolean; 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 3f3f70860f..9ad64b822e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -381,6 +381,26 @@ "within-time": "Within time (sec)", "within-time-pattern": "Time must be a positive integer.", "within-time-required": "Time is required." + }, + "jwt": { + "security-settings": "JWT security settings", + "issuer-name": "Issuer name", + "issuer-name-required": "Issuer name is required.", + "signings-key": "Signing key", + "signings-key-required": "Signing key is required.", + "signings-key-base64": "Signing key must be base64 format.", + "expiration-time": "Token expiration time (sec)", + "expiration-time-required": "Token expiration time is required.", + "expiration-time-pattern": "Token expiration time be a positive integer.", + "expiration-time-min": "Minimum time is 60 seconds (1 minute).", + "refresh-expiration-time": "Refresh token expiration time", + "refresh-expiration-time-required": "Refresh token expiration time is required.", + "refresh-expiration-time-pattern": "Refresh token expiration time be a positive integer.", + "refresh-expiration-time-min": "Minimum time is 900 seconds (15 minute).", + "refresh-expiration-time-less-token": "Refresh token time must be greater token time.", + "generate-key": "Generate key", + "info-header": "All users will be to re-logined", + "info-message": "Change of the JWT Signing Key will cause all issued tokens to be invalid. All users will need to re-login. This will also affect scripts that use Rest API/Websockets." } }, "alarm": { From e32bd456b7875555f0b0b0654288f297e368fa0f Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 14 Nov 2022 11:07:09 +0100 Subject: [PATCH 108/226] added memory usage log to the js-executors --- msa/js-executor/api/jsInvokeMessageProcessor.ts | 5 +++++ msa/js-executor/config/custom-environment-variables.yml | 1 + msa/js-executor/config/default.yml | 1 + 3 files changed, 7 insertions(+) diff --git a/msa/js-executor/api/jsInvokeMessageProcessor.ts b/msa/js-executor/api/jsInvokeMessageProcessor.ts index 668cd61f50..e69209fa01 100644 --- a/msa/js-executor/api/jsInvokeMessageProcessor.ts +++ b/msa/js-executor/api/jsInvokeMessageProcessor.ts @@ -39,6 +39,7 @@ const TIMEOUT_ERROR = 2; const NOT_FOUND_ERROR = 3; const statFrequency = Number(config.get('script.stat_print_frequency')); +const memoryUsageTraceFrequency = Number(config.get('script.memory_usage_trace_frequency')); const scriptBodyTraceFrequency = Number(config.get('script.script_body_trace_frequency')); const useSandbox = config.get('script.use_sandbox') === 'true'; const maxActiveScripts = Number(config.get('script.max_active_scripts')); @@ -167,6 +168,10 @@ export class JsInvokeMessageProcessor { if (this.executedScriptsCounter % scriptBodyTraceFrequency == 0) { this.logger.info('[%s] Executing script body: [%s]', scriptId, invokeRequest.scriptBody); } + if (this.executedScriptsCounter % memoryUsageTraceFrequency == 0) { + this.logger.info('Current memory usage: [%s]', process.memoryUsage()); + } + this.getOrCompileScript(scriptId, invokeRequest.scriptBody).then( (script) => { this.executor.executeScript(script, invokeRequest.args, invokeRequest.timeout).then( diff --git a/msa/js-executor/config/custom-environment-variables.yml b/msa/js-executor/config/custom-environment-variables.yml index b9c24c8d8d..2ebea4ccc1 100644 --- a/msa/js-executor/config/custom-environment-variables.yml +++ b/msa/js-executor/config/custom-environment-variables.yml @@ -75,6 +75,7 @@ logger: script: use_sandbox: "SCRIPT_USE_SANDBOX" + memory_usage_trace_frequency: "MEMORY_USAGE_TRACE_FREQUENCY" stat_print_frequency: "SCRIPT_STAT_PRINT_FREQUENCY" script_body_trace_frequency: "SCRIPT_BODY_TRACE_FREQUENCY" max_active_scripts: "MAX_ACTIVE_SCRIPTS" diff --git a/msa/js-executor/config/default.yml b/msa/js-executor/config/default.yml index 64829ef792..805c175dce 100644 --- a/msa/js-executor/config/default.yml +++ b/msa/js-executor/config/default.yml @@ -64,6 +64,7 @@ logger: script: use_sandbox: "true" + memory_usage_trace_frequency: "10000" script_body_trace_frequency: "10000" stat_print_frequency: "10000" max_active_scripts: "1000" From 11bd9b5257344b60c7e5eb1a2bbe6af175ffde5b Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 14 Nov 2022 11:28:30 +0100 Subject: [PATCH 109/226] added nodejs memory leak workaround --- docker/tb-js-executor.env | 3 ++- msa/js-executor/config/default.yml | 2 +- msa/js-executor/docker/start-js-executor.sh | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docker/tb-js-executor.env b/docker/tb-js-executor.env index e080906549..1938449d53 100644 --- a/docker/tb-js-executor.env +++ b/docker/tb-js-executor.env @@ -3,4 +3,5 @@ LOGGER_LEVEL=info LOG_FOLDER=logs LOGGER_FILENAME=tb-js-executor-%DATE%.log DOCKER_MODE=true -SCRIPT_BODY_TRACE_FREQUENCY=1000 \ No newline at end of file +SCRIPT_BODY_TRACE_FREQUENCY=1000 +NODE_OPTIONS="--max-old-space-size=200" diff --git a/msa/js-executor/config/default.yml b/msa/js-executor/config/default.yml index 805c175dce..96f3401da5 100644 --- a/msa/js-executor/config/default.yml +++ b/msa/js-executor/config/default.yml @@ -64,7 +64,7 @@ logger: script: use_sandbox: "true" - memory_usage_trace_frequency: "10000" + memory_usage_trace_frequency: "1000" script_body_trace_frequency: "10000" stat_print_frequency: "10000" max_active_scripts: "1000" diff --git a/msa/js-executor/docker/start-js-executor.sh b/msa/js-executor/docker/start-js-executor.sh index 575f93c389..b27c1b7167 100755 --- a/msa/js-executor/docker/start-js-executor.sh +++ b/msa/js-executor/docker/start-js-executor.sh @@ -27,4 +27,4 @@ source "${CONF_FOLDER}/${configfile}" cd ${pkg.installFolder} # This will forward this PID 1 to the node.js and forward SIGTERM for graceful shutdown as well -exec node server.js +exec --no-compilation-cache node server.js From b02a2215db8f01b41a18049790d411ba5a3be647 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 14 Nov 2022 12:33:17 +0200 Subject: [PATCH 110/226] Update MVEL version to 2.4.24TB --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 60e8783bca..e7e75c32f8 100755 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ 3.5.5 3.21.9 1.42.1 - 2.4.23TB + 2.4.24TB 1.18.18 1.2.4 4.1.75.Final From 335d88c82eaaf559bfd1b232419a7d6e714c4b67 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 14 Nov 2022 12:27:04 +0100 Subject: [PATCH 111/226] jwt settings added message on base64 validation exception --- .../server/config/jwt/JwtSettingsValidatorDefault.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java index ee60a80de6..66a7311bf3 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.config.jwt; -import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.RandomUtils; import org.apache.commons.lang3.StringUtils; @@ -53,7 +52,7 @@ public class JwtSettingsValidatorDefault implements JwtSettingsValidator { try { decodedKey = Base64.getDecoder().decode(jwtSettings.getTokenSigningKey()); } catch (Exception e) { - throw new DataValidationException("JWT token signing key should be valid Base64 encoded string! " + e.getCause()); + throw new DataValidationException("JWT token signing key should be a valid Base64 encoded string! " + e.getMessage()); } if (Arrays.isNullOrEmpty(decodedKey)) { From 5b1642246d722a67869ee4bb8ecca511cc138608 Mon Sep 17 00:00:00 2001 From: Yevhen Bondarenko <56396344+YevhenBondarenko@users.noreply.github.com> Date: Mon, 14 Nov 2022 13:30:05 +0100 Subject: [PATCH 112/226] [3.4.2] fix start js (#7614) * refactoring * fixed start js-executor typo --- msa/js-executor/docker/start-js-executor.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msa/js-executor/docker/start-js-executor.sh b/msa/js-executor/docker/start-js-executor.sh index b27c1b7167..d30b62c145 100755 --- a/msa/js-executor/docker/start-js-executor.sh +++ b/msa/js-executor/docker/start-js-executor.sh @@ -27,4 +27,4 @@ source "${CONF_FOLDER}/${configfile}" cd ${pkg.installFolder} # This will forward this PID 1 to the node.js and forward SIGTERM for graceful shutdown as well -exec --no-compilation-cache node server.js +exec node --no-compilation-cache server.js From 51ec17d9d1782b755bd8aea10eeac1897c050166 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 14 Nov 2022 14:41:10 +0200 Subject: [PATCH 113/226] Update MVEL version to 2.4.25TB --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e7e75c32f8..5f5aa3703b 100755 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ 3.5.5 3.21.9 1.42.1 - 2.4.24TB + 2.4.25TB 1.18.18 1.2.4 4.1.75.Final From 46b2adeb2cf4ead968df17a0d2e83e91b97f3b94 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 14 Nov 2022 13:43:56 +0100 Subject: [PATCH 114/226] jwt settings added message on base64 validation exception, default key is to be validated ok, tests added --- .../server/config/jwt/JwtSettings.java | 7 + .../config/jwt/JwtSettingsServiceDefault.java | 5 +- .../jwt/JwtSettingsValidatorDefault.java | 4 +- .../controller/BaseAdminControllerTest.java | 123 +++++++++++++----- 4 files changed, 101 insertions(+), 38 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettings.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettings.java index f99b36f32e..2dd846446f 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettings.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettings.java @@ -15,15 +15,22 @@ */ package org.thingsboard.server.config.jwt; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.security.model.JwtToken; @Component @ConfigurationProperties(prefix = "security.jwt") +@AllArgsConstructor +@NoArgsConstructor @Data public class JwtSettings { + static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; + static final String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; + /** * {@link JwtToken} will expire after this time. */ diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java index 73867b1334..6fac3b2ac2 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java @@ -37,13 +37,14 @@ import java.util.Base64; import java.util.Objects; import java.util.Optional; +import static org.thingsboard.server.config.jwt.JwtSettings.ADMIN_SETTINGS_JWT_KEY; +import static org.thingsboard.server.config.jwt.JwtSettings.TOKEN_SIGNING_KEY_DEFAULT; + @Service @RequiredArgsConstructor @Slf4j public class JwtSettingsServiceDefault implements JwtSettingsService { - static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; - static final String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; @Lazy private final AdminSettingsService adminSettingsService; @Lazy diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java index 66a7311bf3..840da625e6 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java @@ -26,6 +26,8 @@ import java.util.Base64; import java.util.Optional; import java.util.concurrent.TimeUnit; +import static org.thingsboard.server.config.jwt.JwtSettings.TOKEN_SIGNING_KEY_DEFAULT; + @Component @RequiredArgsConstructor public class JwtSettingsValidatorDefault implements JwtSettingsValidator { @@ -58,7 +60,7 @@ public class JwtSettingsValidatorDefault implements JwtSettingsValidator { if (Arrays.isNullOrEmpty(decodedKey)) { throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!"); } - if (decodedKey.length * Byte.SIZE < 256) { + if (decodedKey.length * Byte.SIZE < 256 && !TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) { throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 256 bits of data!"); } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java index 81ecce8300..93269499d9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java @@ -17,14 +17,22 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.config.jwt.JwtSettings; +import org.thingsboard.server.config.jwt.JwtSettingsService; import org.thingsboard.server.service.mail.DefaultMailService; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -32,8 +40,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - +@Slf4j public abstract class BaseAdminControllerTest extends AbstractControllerTest { + final JwtSettings defaultJwtSettings = new JwtSettings(9000, "thingsboard.io", "thingsboardDefaultSigningKey", 604800); @Autowired MailService mailService; @@ -45,67 +54,67 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest { public void testFindAdminSettingsByKey() throws Exception { loginSysAdmin(); doGet("/api/admin/settings/general") - .andExpect(status().isOk()) - .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$.id", notNullValue())) - .andExpect(jsonPath("$.key", is("general"))) - .andExpect(jsonPath("$.jsonValue.baseUrl", is("http://localhost:8080"))); - + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.id", notNullValue())) + .andExpect(jsonPath("$.key", is("general"))) + .andExpect(jsonPath("$.jsonValue.baseUrl", is("http://localhost:8080"))); + doGet("/api/admin/settings/mail") - .andExpect(status().isOk()) - .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$.id", notNullValue())) - .andExpect(jsonPath("$.key", is("mail"))) - .andExpect(jsonPath("$.jsonValue.smtpProtocol", is("smtp"))) - .andExpect(jsonPath("$.jsonValue.smtpHost", is("localhost"))) - .andExpect(jsonPath("$.jsonValue.smtpPort", is("25"))); - + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.id", notNullValue())) + .andExpect(jsonPath("$.key", is("mail"))) + .andExpect(jsonPath("$.jsonValue.smtpProtocol", is("smtp"))) + .andExpect(jsonPath("$.jsonValue.smtpHost", is("localhost"))) + .andExpect(jsonPath("$.jsonValue.smtpPort", is("25"))); + doGet("/api/admin/settings/unknown") - .andExpect(status().isNotFound()); - + .andExpect(status().isNotFound()); + } - + @Test public void testSaveAdminSettings() throws Exception { loginSysAdmin(); - AdminSettings adminSettings = doGet("/api/admin/settings/general", AdminSettings.class); - + AdminSettings adminSettings = doGet("/api/admin/settings/general", AdminSettings.class); + JsonNode jsonValue = adminSettings.getJsonValue(); ((ObjectNode) jsonValue).put("baseUrl", "http://myhost.org"); adminSettings.setJsonValue(jsonValue); doPost("/api/admin/settings", adminSettings).andExpect(status().isOk()); - + doGet("/api/admin/settings/general") - .andExpect(status().isOk()) - .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$.jsonValue.baseUrl", is("http://myhost.org"))); - + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.jsonValue.baseUrl", is("http://myhost.org"))); + ((ObjectNode) jsonValue).put("baseUrl", "http://localhost:8080"); adminSettings.setJsonValue(jsonValue); - + doPost("/api/admin/settings", adminSettings) - .andExpect(status().isOk()); + .andExpect(status().isOk()); } @Test public void testSaveAdminSettingsWithEmptyKey() throws Exception { loginSysAdmin(); - AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); + AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); adminSettings.setKey(null); doPost("/api/admin/settings", adminSettings) - .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Key should be specified"))); + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Key should be specified"))); } - + @Test public void testChangeAdminSettingsKey() throws Exception { loginSysAdmin(); - AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); + AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); adminSettings.setKey("newKey"); doPost("/api/admin/settings", adminSettings) - .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("is prohibited"))); + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("is prohibited"))); } @Test @@ -113,7 +122,7 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest { loginSysAdmin(); AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); doPost("/api/admin/settings/testMail", adminSettings) - .andExpect(status().isOk()); + .andExpect(status().isOk()); } @Test @@ -139,4 +148,48 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest { doPost("/api/admin/settings/testMail", adminSettings).andExpect(status().is5xxServerError()); Mockito.doNothing().when(mailService).sendTestMail(Mockito.any(), Mockito.any()); } + + void resetJwtSettingsToDefault() throws Exception { + loginSysAdmin(); + doPost("/api/admin/jwtSettings", defaultJwtSettings).andExpect(status().isOk()); // jwt test scenarios are always started from + loginTenantAdmin(); + } + + @Test + public void testGetAndSaveDefaultJwtSettings() throws Exception { + JwtSettings jwtSettings; + loginSysAdmin(); + + jwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class); + assertThat(jwtSettings).isEqualTo(defaultJwtSettings); + + doPost("/api/admin/jwtSettings", jwtSettings).andExpect(status().isOk()); + + jwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class); + assertThat(jwtSettings).isEqualTo(defaultJwtSettings); + + resetJwtSettingsToDefault(); + } + + @Test + public void testCreateJwtSettings() throws Exception { + loginSysAdmin(); + + JwtSettings jwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class); + assertThat(jwtSettings).isEqualTo(defaultJwtSettings); + + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( + RandomStringUtils.randomAlphanumeric(256 / Byte.SIZE).getBytes(StandardCharsets.UTF_8))); + + doPost("/api/admin/jwtSettings", jwtSettings).andExpect(status().isOk()); + + doGet("/api/admin/jwtSettings").andExpect(status().isUnauthorized()); //the old JWT token does not work after signing key was changed! + + loginSysAdmin(); + JwtSettings newJwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class); + assertThat(jwtSettings).isEqualTo(newJwtSettings); + + resetJwtSettingsToDefault(); + } + } From 1d230f19451f1aa2178a818e0763330152b23af9 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 14 Nov 2022 15:03:55 +0200 Subject: [PATCH 115/226] UI: Fixed generate length signing key --- .../modules/home/pages/admin/security-settings.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts index bdf645a9d7..554d8550ff 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts @@ -126,7 +126,7 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').value !== (this.jwtSettings?.tokenSigningKey || '')) { return this.dialogService.confirm( this.translate.instant('admin.jwt.info-header'), - `
${this.translate.instant('admin.jwt.info-message')}
`, + `
${this.translate.instant('admin.jwt.info-message')}
`, this.translate.instant('action.discard-changes'), this.translate.instant('action.confirm') ); @@ -135,7 +135,7 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi } generateSigningKey() { - this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').setValue(randomAlphanumeric(44)); + this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').setValue(randomAlphanumeric(64)); if (this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').pristine) { this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').markAsDirty(); this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').markAsTouched(); From dde62fc51817f109d893713f8d7cd322ef45eaa6 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 14 Nov 2022 15:16:33 +0200 Subject: [PATCH 116/226] UI: Fixed generate length signing key --- .../modules/home/pages/admin/security-settings.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts index 554d8550ff..903bbb3bac 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts @@ -135,7 +135,7 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi } generateSigningKey() { - this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').setValue(randomAlphanumeric(64)); + this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').setValue(btoa(randomAlphanumeric(64))); if (this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').pristine) { this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').markAsDirty(); this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').markAsTouched(); @@ -169,7 +169,7 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi private base64Format(control: FormControl): { [key: string]: boolean } | null { try { - const value = btoa(control.value); + const value = atob(control.value); return null; } catch (e) { return {base64: true}; From 25f8ff2aefed66c17adb558546cf7bc575c31257 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 14 Nov 2022 15:37:54 +0200 Subject: [PATCH 117/226] Handle undefined script execution result. --- msa/js-executor/api/jsExecutor.models.ts | 2 +- msa/js-executor/api/jsInvokeMessageProcessor.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/msa/js-executor/api/jsExecutor.models.ts b/msa/js-executor/api/jsExecutor.models.ts index 7a6b53cd8a..17407f4d50 100644 --- a/msa/js-executor/api/jsExecutor.models.ts +++ b/msa/js-executor/api/jsExecutor.models.ts @@ -56,7 +56,7 @@ export interface JsCompileResponse extends TbMessage { export interface JsInvokeResponse { success: boolean; - result: string; + result?: string; errorCode?: number; errorDetails?: string; } diff --git a/msa/js-executor/api/jsInvokeMessageProcessor.ts b/msa/js-executor/api/jsInvokeMessageProcessor.ts index e69209fa01..52a337c74a 100644 --- a/msa/js-executor/api/jsInvokeMessageProcessor.ts +++ b/msa/js-executor/api/jsInvokeMessageProcessor.ts @@ -175,8 +175,8 @@ export class JsInvokeMessageProcessor { this.getOrCompileScript(scriptId, invokeRequest.scriptBody).then( (script) => { this.executor.executeScript(script, invokeRequest.args, invokeRequest.timeout).then( - (result) => { - if (result.length <= maxResultSize) { + (result: string | undefined) => { + if (!result || result.length <= maxResultSize) { const invokeResponse = JsInvokeMessageProcessor.createInvokeResponse(result, true); this.logger.debug('[%s] Sending success invoke response, scriptId: [%s]', requestId, scriptId); this.sendResponse(requestId, responseTopic, headers, scriptId, undefined, invokeResponse); @@ -328,7 +328,7 @@ export class JsInvokeMessageProcessor { } } - private static createInvokeResponse(result: string, success: boolean, errorCode?: number, err?: any): JsInvokeResponse { + private static createInvokeResponse(result: string | undefined, success: boolean, errorCode?: number, err?: any): JsInvokeResponse { return { errorCode: errorCode, success: success, From dee81fea2653ab22aa9ae97c7b9dd33c1cb0b60a Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 14 Nov 2022 18:00:19 +0200 Subject: [PATCH 118/226] MVEL scripts caching --- .../src/main/resources/thingsboard.yml | 1 + .../service/script/MvelInvokeServiceTest.java | 98 +++++++++++++++++++ common/script/script-api/pom.xml | 4 + .../api/mvel/DefaultMvelInvokeService.java | 65 ++++++++++-- .../script/api/mvel/MvelScript.java | 1 - 5 files changed, 158 insertions(+), 11 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 0f110da90c..cd05c7ff23 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -625,6 +625,7 @@ mvel: max_black_list_duration_sec: "${MVEL_MAX_BLACKLIST_DURATION_SEC:60}" # Specify thread pool size for javascript executor service thread_pool_size: "${MVEL_THREAD_POOL_SIZE:50}" + compiled_scripts_cache_size: "${MVEL_COMPILED_SCRIPTS_CACHE_SIZE:2000}" stats: enabled: "${TB_MVEL_STATS_ENABLED:false}" print_interval_ms: "${TB_MVEL_STATS_PRINT_INTERVAL_MS:10000}" diff --git a/application/src/test/java/org/thingsboard/server/service/script/MvelInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/MvelInvokeServiceTest.java index a40a087514..1c3c134a82 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/MvelInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/MvelInvokeServiceTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.script; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.benmanes.caffeine.cache.Cache; import org.junit.Assert; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -24,15 +25,22 @@ import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.ScriptType; import org.thingsboard.script.api.mvel.MvelInvokeService; +import org.thingsboard.script.api.mvel.MvelScript; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @DaoSqlTest @@ -41,6 +49,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; "mvel.max_total_args_size=50", "mvel.max_result_size=50", "mvel.max_errors=2", + "mvel.compiled_scripts_cache_size=100" }) class MvelInvokeServiceTest extends AbstractControllerTest { @@ -110,6 +119,89 @@ class MvelInvokeServiceTest extends AbstractControllerTest { assertThatScriptIsBlocked(scriptId); } + @Test + void givenScriptsWithSameBody_thenCompileAndCacheOnlyOnce() throws Exception { + String script = "return msg.temperature > 20;"; + List scriptsIds = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + UUID scriptId = evalScript(script); + scriptsIds.add(scriptId); + } + + Map scriptIdToHash = getFieldValue(invokeService, "scriptIdToHash"); + Map scriptMap = getFieldValue(invokeService, "scriptMap"); + Cache compiledScriptsCache = getFieldValue(invokeService, "compiledScriptsCache"); + + String scriptHash = scriptIdToHash.get(scriptsIds.get(0)); + + assertThat(scriptsIds.stream().map(scriptIdToHash::get)).containsOnly(scriptHash); + assertThat(scriptMap).containsKey(scriptHash); + assertThat(compiledScriptsCache.getIfPresent(scriptHash)).isNotNull(); + } + + @Test + public void whenReleasingScript_thenCheckForScriptHashUsages() throws Exception { + String script = "return msg.temperature > 20;"; + List scriptsIds = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + UUID scriptId = evalScript(script); + scriptsIds.add(scriptId); + } + + Map scriptIdToHash = getFieldValue(invokeService, "scriptIdToHash"); + Map scriptMap = getFieldValue(invokeService, "scriptMap"); + Cache compiledScriptsCache = getFieldValue(invokeService, "compiledScriptsCache"); + + String scriptHash = scriptIdToHash.get(scriptsIds.get(0)); + for (int i = 0; i < 9; i++) { + UUID scriptId = scriptsIds.get(i); + assertThat(scriptIdToHash).containsKey(scriptId); + invokeService.release(scriptId); + assertThat(scriptIdToHash).doesNotContainKey(scriptId); + } + assertThat(scriptMap).containsKey(scriptHash); + assertThat(compiledScriptsCache.getIfPresent(scriptHash)).isNotNull(); + + invokeService.release(scriptsIds.get(9)); + assertThat(scriptMap).doesNotContainKey(scriptHash); + assertThat(compiledScriptsCache.getIfPresent(scriptHash)).isNull(); + } + + @Test + public void whenCompiledScriptsCacheIsTooBig_thenRemoveRarelyUsedScripts() throws Exception { + Map scriptIdToHash = getFieldValue(invokeService, "scriptIdToHash"); + Cache compiledScriptsCache = getFieldValue(invokeService, "compiledScriptsCache"); + + List scriptsIds = new ArrayList<>(); + for (int i = 0; i < 110; i++) { // mvel.compiled_scripts_cache_size = 100 + String script = "return msg.temperature > " + i; + UUID scriptId = evalScript(script); + scriptsIds.add(scriptId); + + for (int j = 0; j < i; j++) { + invokeScript(scriptId, "{ \"temperature\": 12 }"); // so that scriptsIds is ordered by number of invocations + } + } + + ConcurrentMap cache = compiledScriptsCache.asMap(); + + for (int i = 0; i < 10; i++) { // iterating rarely used scripts + UUID scriptId = scriptsIds.get(i); + String scriptHash = scriptIdToHash.get(scriptId); + assertThat(cache).doesNotContainKey(scriptHash); + } + for (int i = 10; i < 110; i++) { + UUID scriptId = scriptsIds.get(i); + String scriptHash = scriptIdToHash.get(scriptId); + assertThat(cache).containsKey(scriptHash); + } + + UUID scriptRemovedFromCache = scriptsIds.get(0); + assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNull(); + invokeScript(scriptRemovedFromCache, "{ \"temperature\": 12 }"); + assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNotNull(); + } + private void assertThatScriptIsBlocked(UUID scriptId) { assertThatThrownBy(() -> { invokeScript(scriptId, "{}"); @@ -125,4 +217,10 @@ class MvelInvokeServiceTest extends AbstractControllerTest { return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", "POST_TELEMETRY_REQUEST").get().toString(); } + private T getFieldValue(Object target, String fieldName) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return (T) field.get(target); + } + } diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index 62dc00ca97..b7ae4993f9 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -56,6 +56,10 @@ com.google.code.gson gson + + com.github.ben-manes.caffeine + caffeine + org.slf4j slf4j-api diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java index 901a49180e..120be2af4f 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java @@ -15,6 +15,10 @@ */ package org.thingsboard.script.api.mvel; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; @@ -42,6 +46,7 @@ import org.thingsboard.server.common.stats.TbApiUsageStateClient; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.io.Serializable; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Map; import java.util.Optional; @@ -55,7 +60,10 @@ import java.util.regex.Pattern; @Service public class DefaultMvelInvokeService extends AbstractScriptInvokeService implements MvelInvokeService { - protected Map scriptMap = new ConcurrentHashMap<>(); + protected final Map scriptIdToHash = new ConcurrentHashMap<>(); + protected final Map scriptMap = new ConcurrentHashMap<>(); + protected Cache compiledScriptsCache; + private SandboxedParserConfiguration parserConfig; private static final Pattern NEW_KEYWORD_PATTERN = Pattern.compile("new\\s"); @@ -92,6 +100,9 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem @Value("${mvel.max_memory_limit_mb:8}") private long maxMemoryLimitMb; + @Value("${mvel.compiled_scripts_cache_size:2000}") + private int compiledScriptsCacheSize; + private ListeningExecutorService executor; protected DefaultMvelInvokeService(Optional apiUsageStateClient, Optional apiUsageReportClient) { @@ -115,11 +126,14 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "mvel-executor")); try { // Special command to warm up MVEL engine - Serializable script = MVEL.compileExpression("var warmUp = {}; warmUp", new SandboxedParserContext(parserConfig)); + Serializable script = compileScript("var warmUp = {}; warmUp"); MVEL.executeTbExpression(script, new ExecutionContext(parserConfig), Collections.emptyMap()); } catch (Exception e) { // do nothing } + compiledScriptsCache = Caffeine.newBuilder() + .maximumSize(compiledScriptsCacheSize) + .build(); } @PreDestroy @@ -141,16 +155,21 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem @Override protected boolean isScriptPresent(UUID scriptId) { - return scriptMap.containsKey(scriptId); + return scriptIdToHash.containsKey(scriptId); } @Override protected ListenableFuture doEvalScript(TenantId tenantId, ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames) { return executor.submit(() -> { try { - Serializable compiledScript = MVEL.compileExpression(scriptBody, new SandboxedParserContext(parserConfig)); - MvelScript script = new MvelScript(compiledScript, scriptBody, argNames); - scriptMap.put(scriptId, script); + String scriptHash = hash(scriptBody, argNames); + compiledScriptsCache.get(scriptHash, k -> { + return compileScript(scriptBody); + }); + scriptIdToHash.put(scriptId, scriptHash); + scriptMap.computeIfAbsent(scriptHash, k -> { + return new MvelScript(scriptBody, argNames); + }); return scriptId; } catch (Exception e) { throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e); @@ -162,12 +181,16 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem protected MvelScriptExecutionTask doInvokeFunction(UUID scriptId, Object[] args) { ExecutionContext executionContext = new ExecutionContext(this.parserConfig, maxMemoryLimitMb * 1024 * 1024); return new MvelScriptExecutionTask(executionContext, executor.submit(() -> { - MvelScript script = scriptMap.get(scriptId); - if (script == null) { + String scriptHash = scriptIdToHash.get(scriptId); + if (scriptHash == null) { throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, new RuntimeException("Script not found!")); } + MvelScript script = scriptMap.get(scriptHash); + Serializable compiledScript = compiledScriptsCache.get(scriptHash, k -> { + return compileScript(script.getScriptBody()); + }); try { - return MVEL.executeTbExpression(script.getCompiledScript(), executionContext, script.createVars(args)); + return MVEL.executeTbExpression(compiledScript, executionContext, script.createVars(args)); } catch (ScriptMemoryOverflowException e) { throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, script.getScriptBody(), new RuntimeException("Script memory overflow!")); } catch (Exception e) { @@ -178,6 +201,28 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem @Override protected void doRelease(UUID scriptId) throws Exception { - scriptMap.remove(scriptId); + String scriptHash = scriptIdToHash.remove(scriptId); + if (scriptHash != null) { + if (scriptIdToHash.containsValue(scriptHash)) { + return; + } + scriptMap.remove(scriptHash); + compiledScriptsCache.invalidate(scriptHash); + } + } + + private Serializable compileScript(String scriptBody) { + return MVEL.compileExpression(scriptBody, new SandboxedParserContext(parserConfig)); } + + @SuppressWarnings("UnstableApiUsage") + protected String hash(String scriptBody, String[] argNames) { + Hasher hasher = Hashing.murmur3_128().newHasher(); + hasher.putUnencodedChars(scriptBody); + for (String argName : argNames) { + hasher.putString(argName, StandardCharsets.UTF_8); + } + return hasher.hash().toString(); + } + } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScript.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScript.java index 7a84c7b0af..bca5d8d546 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScript.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScript.java @@ -24,7 +24,6 @@ import java.util.Map; @Data public class MvelScript { - private final Serializable compiledScript; private final String scriptBody; private final String[] argNames; From 6e12a168c036ec9e348b8460196806b99ac5ded4 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 14 Nov 2022 18:57:52 +0200 Subject: [PATCH 119/226] UI: Fixed signing key validators --- .../home/pages/admin/security-settings.component.html | 9 +++++---- .../home/pages/admin/security-settings.component.ts | 7 +++++++ ui-ngx/src/assets/locale/locale.constant-en_US.json | 4 +++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html index 7a498a0f20..86f6f9b8c7 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html @@ -165,9 +165,6 @@ admin.jwt.security-settings
- - -
@@ -181,7 +178,7 @@ admin.jwt.signings-key - + + admin.jwt.signings-key-hint {{ 'admin.jwt.signings-key-required' | translate }} {{ 'admin.jwt.signings-key-base64' | translate }} + + {{ 'admin.jwt.signings-key-min-length' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts index 903bbb3bac..dc7e9c05b7 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts @@ -121,6 +121,10 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi this.jwtSecuritySettingsFormGroup.reset(this.jwtSettings); } + markAsTouched() { + this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').markAsTouched(); + } + private confirmChangeJWTSettings(): Observable { if (this.jwtSecuritySettingsFormGroup.get('tokenIssuer').value !== (this.jwtSettings?.tokenIssuer || '') || this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').value !== (this.jwtSettings?.tokenSigningKey || '')) { @@ -170,6 +174,9 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi private base64Format(control: FormControl): { [key: string]: boolean } | null { try { const value = atob(control.value); + if (value.length < 32 && control.value !== 'thingsboardDefaultSigningKey') { + return {minLength: true}; + } return null; } catch (e) { return {base64: true}; 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 9ad64b822e..511d9def5d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -387,13 +387,15 @@ "issuer-name": "Issuer name", "issuer-name-required": "Issuer name is required.", "signings-key": "Signing key", + "signings-key-hint": "Base64 encoded string representing at least 256 bits of data.", "signings-key-required": "Signing key is required.", + "signings-key-min-length": "Signing key must be at least 256 bits of data.", "signings-key-base64": "Signing key must be base64 format.", "expiration-time": "Token expiration time (sec)", "expiration-time-required": "Token expiration time is required.", "expiration-time-pattern": "Token expiration time be a positive integer.", "expiration-time-min": "Minimum time is 60 seconds (1 minute).", - "refresh-expiration-time": "Refresh token expiration time", + "refresh-expiration-time": "Refresh token expiration time (sec)", "refresh-expiration-time-required": "Refresh token expiration time is required.", "refresh-expiration-time-pattern": "Refresh token expiration time be a positive integer.", "refresh-expiration-time-min": "Minimum time is 900 seconds (15 minute).", From 12830936815a5fed8c141fb771a44265378fe94f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 14 Nov 2022 19:02:52 +0200 Subject: [PATCH 120/226] Synchronize doEvalScript and doRelease operations in MvelInvokeService --- .../src/main/resources/thingsboard.yml | 2 +- .../api/mvel/DefaultMvelInvokeService.java | 31 +++++++++++++------ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index cd05c7ff23..28149246c9 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -625,7 +625,7 @@ mvel: max_black_list_duration_sec: "${MVEL_MAX_BLACKLIST_DURATION_SEC:60}" # Specify thread pool size for javascript executor service thread_pool_size: "${MVEL_THREAD_POOL_SIZE:50}" - compiled_scripts_cache_size: "${MVEL_COMPILED_SCRIPTS_CACHE_SIZE:2000}" + compiled_scripts_cache_size: "${MVEL_COMPILED_SCRIPTS_CACHE_SIZE:1000}" stats: enabled: "${TB_MVEL_STATS_ENABLED:false}" print_interval_ms: "${TB_MVEL_STATS_PRINT_INTERVAL_MS:10000}" diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java index 120be2af4f..4fb029096b 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java @@ -53,6 +53,8 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Pattern; @Slf4j @@ -100,11 +102,13 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem @Value("${mvel.max_memory_limit_mb:8}") private long maxMemoryLimitMb; - @Value("${mvel.compiled_scripts_cache_size:2000}") + @Value("${mvel.compiled_scripts_cache_size:1000}") private int compiledScriptsCacheSize; private ListeningExecutorService executor; + private final Lock lock = new ReentrantLock(); + protected DefaultMvelInvokeService(Optional apiUsageStateClient, Optional apiUsageReportClient) { super(apiUsageStateClient, apiUsageReportClient); } @@ -166,10 +170,15 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem compiledScriptsCache.get(scriptHash, k -> { return compileScript(scriptBody); }); - scriptIdToHash.put(scriptId, scriptHash); - scriptMap.computeIfAbsent(scriptHash, k -> { - return new MvelScript(scriptBody, argNames); - }); + lock.lock(); + try { + scriptIdToHash.put(scriptId, scriptHash); + scriptMap.computeIfAbsent(scriptHash, k -> { + return new MvelScript(scriptBody, argNames); + }); + } finally { + lock.unlock(); + } return scriptId; } catch (Exception e) { throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e); @@ -203,11 +212,15 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem protected void doRelease(UUID scriptId) throws Exception { String scriptHash = scriptIdToHash.remove(scriptId); if (scriptHash != null) { - if (scriptIdToHash.containsValue(scriptHash)) { - return; + lock.lock(); + try { + if (!scriptIdToHash.containsValue(scriptHash)) { + scriptMap.remove(scriptHash); + compiledScriptsCache.invalidate(scriptHash); + } + } finally { + lock.unlock(); } - scriptMap.remove(scriptHash); - compiledScriptsCache.invalidate(scriptHash); } } From 7e5ab3bff70e133bb4477db346a9103f203ebfc5 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 14 Nov 2022 18:52:57 +0100 Subject: [PATCH 121/226] JWT settings refactored packages, Swagger documented, rest client getJwtSettings and saveJwtSettings added --- .../server/ThingsboardInstallApplication.java | 2 +- .../server/controller/AdminController.java | 8 +++--- .../server/controller/AuthController.java | 6 ++--- .../controller/TwoFactorAuthController.java | 6 ++--- .../server/controller/UserController.java | 4 +-- .../DefaultSystemDataLoaderService.java | 2 +- .../queue/DefaultTbCoreConsumerService.java | 2 +- .../processing/AbstractConsumerService.java | 2 +- .../jwt/settings}/JwtSettingsService.java | 4 ++- .../settings}/JwtSettingsServiceDefault.java | 24 +++++++++++++---- .../jwt/settings}/JwtSettingsValidator.java | 7 ++++- .../JwtSettingsValidatorDefault.java | 5 ++-- .../JwtSettingsValidatorInstall.java | 3 ++- .../Oauth2AuthenticationSuccessHandler.java | 4 +-- ...RestAwareAuthenticationSuccessHandler.java | 4 +-- .../security/model/token/JwtTokenFactory.java | 8 +++--- .../controller/BaseAdminControllerTest.java | 5 ++-- .../server/controller/TwoFactorAuthTest.java | 4 +-- .../security/auth/JwtTokenFactoryTest.java | 4 +-- .../common/data/security/model/JwtPair.java | 9 +++---- .../data/security/model}/JwtSettings.java | 26 +++++++++---------- .../thingsboard/rest/client/RestClient.java | 19 ++++++++++++++ 22 files changed, 98 insertions(+), 60 deletions(-) rename application/src/main/java/org/thingsboard/server/{config/jwt => service/security/auth/jwt/settings}/JwtSettingsService.java (85%) rename application/src/main/java/org/thingsboard/server/{config/jwt => service/security/auth/jwt/settings}/JwtSettingsServiceDefault.java (84%) rename application/src/main/java/org/thingsboard/server/{config/jwt => service/security/auth/jwt/settings}/JwtSettingsValidator.java (73%) rename application/src/main/java/org/thingsboard/server/{config/jwt => service/security/auth/jwt/settings}/JwtSettingsValidatorDefault.java (95%) rename application/src/main/java/org/thingsboard/server/{config/jwt => service/security/auth/jwt/settings}/JwtSettingsValidatorInstall.java (89%) rename application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java => common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java (85%) rename {application/src/main/java/org/thingsboard/server/config/jwt => common/data/src/main/java/org/thingsboard/server/common/data/security/model}/JwtSettings.java (65%) diff --git a/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java b/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java index 6009dafafa..b4a0e019b6 100644 --- a/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java +++ b/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java @@ -29,10 +29,10 @@ import java.util.Arrays; @ComponentScan({"org.thingsboard.server.install", "org.thingsboard.server.service.component", "org.thingsboard.server.service.install", + "org.thingsboard.server.service.security.auth.jwt.settings", "org.thingsboard.server.dao", "org.thingsboard.server.common.stats", "org.thingsboard.server.common.transport.config.ssl", - "org.thingsboard.server.config.jwt", "org.thingsboard.server.cache", "org.thingsboard.server.springfox" }) diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 2712f58439..29f32b8ac9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -39,11 +39,11 @@ import org.thingsboard.server.common.data.sms.config.TestSmsRequest; import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo; -import org.thingsboard.server.config.jwt.JwtSettings; -import org.thingsboard.server.config.jwt.JwtSettingsService; +import org.thingsboard.server.common.data.security.model.JwtSettings; +import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.security.model.JwtTokenPair; +import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.permission.Operation; @@ -188,7 +188,7 @@ public class AdminController extends BaseController { @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/jwtSettings", method = RequestMethod.POST) @ResponseBody - public JwtTokenPair saveJwtSettings( + public JwtPair saveJwtSettings( @ApiParam(value = "A JSON value representing the JWT Settings.") @RequestBody JwtSettings jwtSettings) throws ThingsboardException { try { diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 8384599c4d..0cb3a3fc92 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -51,7 +51,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.ActivateUserRequest; import org.thingsboard.server.service.security.model.ChangePasswordRequest; -import org.thingsboard.server.service.security.model.JwtTokenPair; +import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest; import org.thingsboard.server.service.security.model.ResetPasswordRequest; import org.thingsboard.server.service.security.model.SecurityUser; @@ -236,7 +236,7 @@ public class AuthController extends BaseController { @RequestMapping(value = "/noauth/activate", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) @ResponseBody - public JwtTokenPair activateUser( + public JwtPair activateUser( @ApiParam(value = "Activate user request.") @RequestBody ActivateUserRequest activateRequest, @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, @@ -278,7 +278,7 @@ public class AuthController extends BaseController { @RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) @ResponseBody - public JwtTokenPair resetPassword( + public JwtPair resetPassword( @ApiParam(value = "Reset password request.") @RequestBody ResetPasswordRequest resetPasswordRequest, HttpServletRequest request) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index 003b4ab450..5ce46e324e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -39,7 +39,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; -import org.thingsboard.server.service.security.model.JwtTokenPair; +import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.system.SystemSecurityService; @@ -87,8 +87,8 @@ public class TwoFactorAuthController extends BaseController { "and Too Many Requests error if rate limits are exceeded.") @PostMapping("/verification/check") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") - public JwtTokenPair checkTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType, - @RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception { + public JwtPair checkTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType, + @RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true); if (verificationSuccess) { diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index cea9d2e95a..a2b3a6d6bc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -46,7 +46,7 @@ import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.user.TbUserService; -import org.thingsboard.server.service.security.model.JwtTokenPair; +import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; @@ -145,7 +145,7 @@ public class UserController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/user/{userId}/token", method = RequestMethod.GET) @ResponseBody - public JwtTokenPair getUserToken( + public JwtPair getUserToken( @ApiParam(value = USER_ID_PARAM_DESCRIPTION) @PathVariable(USER_ID) String strUserId) throws ThingsboardException { checkParameter(USER_ID, strUserId); diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 961d0265f9..e512fb028e 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -82,7 +82,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.config.jwt.JwtSettingsService; +import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceCredentialsService; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index d10c968364..d0af49b1e0 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.stats.StatsFactory; -import org.thingsboard.server.config.jwt.JwtSettingsService; +import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index 8d3ec4b55a..c814ab1704 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -33,7 +33,7 @@ import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.config.jwt.JwtSettingsService; +import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java similarity index 85% rename from application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java rename to application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java index bb0127a39e..0282e64804 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.config.jwt; +package org.thingsboard.server.service.security.auth.jwt.settings; + +import org.thingsboard.server.common.data.security.model.JwtSettings; public interface JwtSettingsService { diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsServiceDefault.java similarity index 84% rename from application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java rename to application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsServiceDefault.java index 6fac3b2ac2..d5e92554f9 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsServiceDefault.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.config.jwt; +package org.thingsboard.server.service.security.auth.jwt.settings; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -29,6 +29,7 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.dao.settings.AdminSettingsService; import javax.annotation.PostConstruct; @@ -37,8 +38,8 @@ import java.util.Base64; import java.util.Objects; import java.util.Optional; -import static org.thingsboard.server.config.jwt.JwtSettings.ADMIN_SETTINGS_JWT_KEY; -import static org.thingsboard.server.config.jwt.JwtSettings.TOKEN_SIGNING_KEY_DEFAULT; +import static org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsValidator.ADMIN_SETTINGS_JWT_KEY; +import static org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsValidator.TOKEN_SIGNING_KEY_DEFAULT; @Service @RequiredArgsConstructor @@ -52,12 +53,25 @@ public class JwtSettingsServiceDefault implements JwtSettingsService { private final JwtSettingsValidator jwtSettingsValidator; private final Environment environment; @Getter - private final JwtSettings jwtSettings; + private final JwtSettings jwtSettings = new JwtSettings(); @Value("${install.upgrade:false}") private boolean isUpgrade; + @Value("${security.jwt.tokenExpirationTime:9000}") + private Integer tokenExpirationTime; + @Value("${security.jwt.refreshTokenExpTime:604800}") + private Integer refreshTokenExpTime; + @Value("${security.jwt.tokenIssuer:thingsboard.io}") + private String tokenIssuer; + @Value("${security.jwt.tokenSigningKey:thingsboardDefaultSigningKey}") + private String tokenSigningKey; + @PostConstruct public void init() { + jwtSettings.setTokenExpirationTime(this.tokenExpirationTime); + jwtSettings.setRefreshTokenExpTime(this.refreshTokenExpTime); + jwtSettings.setTokenIssuer(this.tokenIssuer); + jwtSettings.setTokenSigningKey(this.tokenSigningKey); if (!isFirstInstall()) { reloadJwtSettings(); } @@ -77,8 +91,8 @@ public class JwtSettingsServiceDefault implements JwtSettingsService { if (adminJwtSettings != null) { log.info("Reloading the JWT admin settings from database"); JwtSettings jwtLoaded = mapAdminToJwtSettings(adminJwtSettings); - jwtSettings.setRefreshTokenExpTime(jwtLoaded.getRefreshTokenExpTime()); jwtSettings.setTokenExpirationTime(jwtLoaded.getTokenExpirationTime()); + jwtSettings.setRefreshTokenExpTime(jwtLoaded.getRefreshTokenExpTime()); jwtSettings.setTokenIssuer(jwtLoaded.getTokenIssuer()); jwtSettings.setTokenSigningKey(jwtLoaded.getTokenSigningKey()); } diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java similarity index 73% rename from application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java rename to application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java index 8e0d4afe7a..30d23bca00 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java @@ -13,8 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.config.jwt; +package org.thingsboard.server.service.security.auth.jwt.settings; + +import org.thingsboard.server.common.data.security.model.JwtSettings; public interface JwtSettingsValidator { + String ADMIN_SETTINGS_JWT_KEY = "jwt"; + String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; + void validate(JwtSettings jwtSettings); } diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidatorDefault.java similarity index 95% rename from application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java rename to application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidatorDefault.java index 840da625e6..2ebe4e8025 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidatorDefault.java @@ -13,21 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.config.jwt; +package org.thingsboard.server.service.security.auth.jwt.settings; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.RandomUtils; import org.apache.commons.lang3.StringUtils; import org.bouncycastle.util.Arrays; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.dao.exception.DataValidationException; import java.util.Base64; import java.util.Optional; import java.util.concurrent.TimeUnit; -import static org.thingsboard.server.config.jwt.JwtSettings.TOKEN_SIGNING_KEY_DEFAULT; - @Component @RequiredArgsConstructor public class JwtSettingsValidatorDefault implements JwtSettingsValidator { diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorInstall.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidatorInstall.java similarity index 89% rename from application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorInstall.java rename to application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidatorInstall.java index e353eb57a2..a7d097ee39 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorInstall.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidatorInstall.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.config.jwt; +package org.thingsboard.server.service.security.auth.jwt.settings; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.security.model.JwtSettings; @Primary @Profile("install") diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index 9fd2a680b4..9be9d2217b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.OAuth2Registration; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.security.model.JwtTokenPair; +import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; @@ -104,7 +104,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), registration); - JwtTokenPair tokenPair = tokenFactory.createTokenPair(securityUser); + JwtPair tokenPair = tokenFactory.createTokenPair(securityUser); clearAuthenticationAttributes(request, response); getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken()); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java index 4d7ef01914..f6d9fd8666 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java @@ -26,7 +26,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.auth.MfaAuthenticationToken; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; -import org.thingsboard.server.service.security.model.JwtTokenPair; +import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; @@ -49,7 +49,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); - JwtTokenPair tokenPair = new JwtTokenPair(); + JwtPair tokenPair = new JwtPair(); if (authentication instanceof MfaAuthenticationToken) { int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true) diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index b5779dcb44..6da68c37f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -35,9 +35,9 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.config.jwt.JwtSettingsService; +import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; -import org.thingsboard.server.service.security.model.JwtTokenPair; +import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -214,10 +214,10 @@ public class JwtTokenFactory { } } - public JwtTokenPair createTokenPair(SecurityUser securityUser) { + public JwtPair createTokenPair(SecurityUser securityUser) { JwtToken accessToken = createAccessJwtToken(securityUser); JwtToken refreshToken = createRefreshToken(securityUser); - return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken()); + return new JwtPair(accessToken.getToken(), refreshToken.getToken()); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java index 93269499d9..285d647a14 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java @@ -25,8 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.common.data.AdminSettings; -import org.thingsboard.server.config.jwt.JwtSettings; -import org.thingsboard.server.config.jwt.JwtSettingsService; +import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.service.mail.DefaultMailService; import java.nio.charset.StandardCharsets; @@ -42,7 +41,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @Slf4j public abstract class BaseAdminControllerTest extends AbstractControllerTest { - final JwtSettings defaultJwtSettings = new JwtSettings(9000, "thingsboard.io", "thingsboardDefaultSigningKey", 604800); + final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "thingsboardDefaultSigningKey"); @Autowired MailService mailService; diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index 9839843b0c..6c7dfc3cd7 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -51,7 +51,7 @@ import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.auth.rest.LoginRequest; -import org.thingsboard.server.service.security.model.JwtTokenPair; +import org.thingsboard.server.common.data.security.model.JwtPair; import java.time.Duration; import java.util.Arrays; @@ -396,7 +396,7 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest { private void logInWithPreVerificationToken(String username, String password) throws Exception { LoginRequest loginRequest = new LoginRequest(username, password); - JwtTokenPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtTokenPair.class); + JwtPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtPair.class); assertThat(response.getToken()).isNotNull(); assertThat(response.getRefreshToken()).isNull(); assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN); diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index 796ef93c0b..bf89eadda5 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -23,8 +23,8 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.config.jwt.JwtSettings; -import org.thingsboard.server.config.jwt.JwtSettingsService; +import org.thingsboard.server.common.data.security.model.JwtSettings; +import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.AccessJwtToken; diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java similarity index 85% rename from application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java index 02e28cd885..eb50a11a92 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java @@ -13,19 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.model; +package org.thingsboard.server.common.data.security.model; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.security.Authority; -@ApiModel(value = "JWT Token Pair") +@ApiModel(value = "JWT Pair") @Data @NoArgsConstructor -public class JwtTokenPair { +public class JwtPair { @ApiModelProperty(position = 1, value = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..") private String token; @@ -34,7 +33,7 @@ public class JwtTokenPair { private Authority scope; - public JwtTokenPair(String token, String refreshToken) { + public JwtPair(String token, String refreshToken) { this.token = token; this.refreshToken = refreshToken; } diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java similarity index 65% rename from application/src/main/java/org/thingsboard/server/config/jwt/JwtSettings.java rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java index 2dd846446f..f5668ff088 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java @@ -13,43 +13,43 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.config.jwt; +package org.thingsboard.server.common.data.security.model; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.security.model.JwtToken; -@Component -@ConfigurationProperties(prefix = "security.jwt") +@ApiModel(value = "JWT Settings") @AllArgsConstructor @NoArgsConstructor @Data public class JwtSettings { - static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; - static final String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; /** * {@link JwtToken} will expire after this time. */ + @ApiModelProperty(position = 1, value = "The JWT will expire after seconds.", example = "9000") private Integer tokenExpirationTime; + /** + * {@link JwtToken} can be refreshed during this timeframe. + */ + @ApiModelProperty(position = 2, value = "The JWT can be refreshed during seconds.", example = "604800") + private Integer refreshTokenExpTime; + /** * Token issuer. */ + @ApiModelProperty(position = 3, value = "The JWT issuer.", example = "thingsboard.io") private String tokenIssuer; /** * Key is used to sign {@link JwtToken}. * Base64 encoded */ + @ApiModelProperty(position = 4, value = "The JWT key is used to sing token. Base64 encoded.", example = "cTU4WnNqemI2aU5wbWVjdm1vYXRzanhjNHRUcXliMjE=") private String tokenSigningKey; - /** - * {@link JwtToken} can be refreshed during this timeframe. - */ - private Integer refreshTokenExpTime; - } diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 21a18534ed..042453097f 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -136,6 +136,8 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.data.security.model.JwtPair; +import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.common.data.sms.config.TestSmsRequest; @@ -286,6 +288,23 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { return restTemplate.postForEntity(baseURL + "/api/admin/securitySettings", securitySettings, SecuritySettings.class).getBody(); } + public Optional getJwtSettings() { + try { + ResponseEntity jwtSettings = restTemplate.getForEntity(baseURL + "/api/admin/jwtSettings", JwtSettings.class); + return Optional.ofNullable(jwtSettings.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public JwtPair saveJwtSettings(JwtSettings jwtSettings) { + return restTemplate.postForEntity(baseURL + "/api/admin/jwtSettings", jwtSettings, JwtPair.class).getBody(); + } + public Optional getRepositorySettings() { try { ResponseEntity repositorySettings = restTemplate.getForEntity(baseURL + "/api/admin/repositorySettings", RepositorySettings.class); From 1564ba76f1c46a89c35c565c262148f53248ab24 Mon Sep 17 00:00:00 2001 From: Yuriy Lytvynchuk Date: Tue, 15 Nov 2022 09:38:56 +0200 Subject: [PATCH 122/226] add checkEntityType for init node --- .../engine/transform/TbChangeOriginatorNode.java | 1 + .../engine/util/EntitiesByNameAndTypeLoader.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java index 8a801cbb7f..52fa68dba8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java @@ -127,6 +127,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode { log.error("EntityNamePattern not specified for type [{}]", conf.getEntityType()); throw new IllegalArgumentException("Wrong config for [{}] in TbChangeOriginatorNode!" + ENTITY_SOURCE); } + EntitiesByNameAndTypeLoader.checkEntityType(EntityType.valueOf(conf.getEntityType())); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java index 3c3417ad25..21f2cf5822 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java @@ -20,8 +20,17 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.EntityId; +import java.util.List; + public class EntitiesByNameAndTypeLoader { + private static final List AVAILABLE_ENTITY_TYPES = List.of( + EntityType.DEVICE, + EntityType.ASSET, + EntityType.ENTITY_VIEW, + EntityType.EDGE, + EntityType.USER); + public static EntityId findEntityId(TbContext ctx, EntityType entityType, String entityName) { SearchTextBasedWithAdditionalInfo targetEntity; switch (entityType) { @@ -49,4 +58,10 @@ public class EntitiesByNameAndTypeLoader { return targetEntity.getId(); } + public static void checkEntityType(EntityType entityType) { + if (!AVAILABLE_ENTITY_TYPES.contains(entityType)) { + throw new IllegalStateException("Unexpected entity type " + entityType.name()); + } + } + } From 581af4da0f41cb83ab8b6a9e68099ea0f37b26db Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 15 Nov 2022 10:41:39 +0200 Subject: [PATCH 123/226] UI: Minor improvement style --- .../home/pages/admin/security-settings.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html index 86f6f9b8c7..e246d9ef40 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html @@ -167,8 +167,8 @@ -
-
+
+
admin.jwt.issuer-name @@ -198,7 +198,7 @@
-
+
admin.jwt.expiration-time Date: Tue, 15 Nov 2022 11:29:12 +0200 Subject: [PATCH 124/226] UI: Minor fix style and fix signing key validation --- .../home/pages/admin/security-settings.component.html | 1 + .../modules/home/pages/admin/security-settings.component.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html index e246d9ef40..39db84d8c6 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html @@ -180,6 +180,7 @@ admin.jwt.signings-key