diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 4ee86871d6..9ecb9dfcc9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.mobile.QrCodeSettingService; +import org.thingsboard.server.dao.trendz.TrendzSettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -87,6 +88,9 @@ public class SystemInfoController extends BaseController { @Autowired private DebugModeRateLimitsConfig debugModeRateLimitsConfig; + @Autowired + private TrendzSettingsService trendzSettingsService; + @PostConstruct public void init() { JsonNode info = buildInfoObject(); @@ -162,6 +166,7 @@ public class SystemInfoController extends BaseController { systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) .map(QrCodeSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage) .orElse(false)); + systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId())); return systemParams; } diff --git a/application/src/main/java/org/thingsboard/server/controller/TrendzController.java b/application/src/main/java/org/thingsboard/server/controller/TrendzController.java new file mode 100644 index 0000000000..4430f740c7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/TrendzController.java @@ -0,0 +1,80 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.trendz.TrendzSettings; +import org.thingsboard.server.config.annotations.ApiOperation; +import org.thingsboard.server.dao.trendz.TrendzSettingsService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; + +@RestController +@TbCoreComponent +@RequiredArgsConstructor +@RequestMapping("/api") +public class TrendzController extends BaseController { + + private final TrendzSettingsService trendzSettingsService; + + @ApiOperation(value = "Save Trendz settings (saveTrendzSettings)", + notes = "Saves Trendz settings for this tenant or sysadmin.\n" + NEW_LINE + + "Here is an example of the Trendz settings:\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"enabled\": true,\n" + + " \"trendzUrl\": \"https://some.domain.com:18888/also_necessary_prefix\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PostMapping("/trendz/settings") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public TrendzSettings saveTrendzSettings(@RequestBody TrendzSettings trendzSettings, + @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + accessControlService.checkPermission(user, Resource.TRENDZ_SETTINGS, Operation.WRITE); + TenantId tenantId = user.isSystemAdmin() ? TenantId.SYS_TENANT_ID : user.getTenantId(); + trendzSettingsService.saveTrendzSettings(tenantId, trendzSettings); + return trendzSettings; + } + + @ApiOperation(value = "Get Trendz Settings (getTrendzSettings)", + notes = "Retrieves trendz settings for this tenant or sysadmin." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping("/trendz/settings") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + public TrendzSettings getTrendzSettings(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + accessControlService.checkPermission(user, Resource.TRENDZ_SETTINGS, Operation.READ); + TenantId tenantId = user.isSystemAdmin() ? TenantId.SYS_TENANT_ID : user.getTenantId(); + return trendzSettingsService.findTrendzSettings(tenantId); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java index 8124671cd7..ea7457ec47 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java @@ -48,6 +48,7 @@ public class CustomerUserPermissions extends AbstractPermissions { put(Resource.ASSET_PROFILE, profilePermissionChecker); put(Resource.TB_RESOURCE, customerResourcePermissionChecker); put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ)); + put(Resource.TRENDZ_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ)); } private static final PermissionChecker customerAlarmPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index 9d7590f786..1added11b0 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -51,7 +51,8 @@ public enum Resource { NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), MOBILE_APP_SETTINGS, - CALCULATED_FIELD(EntityType.CALCULATED_FIELD); + CALCULATED_FIELD(EntityType.CALCULATED_FIELD), + TRENDZ_SETTINGS; private final Set entityTypes; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index e64f5b49dd..acfec9da25 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.model.SecurityUser; -@Component(value="sysAdminPermissions") +@Component(value = "sysAdminPermissions") public class SysAdminPermissions extends AbstractPermissions { public SysAdminPermissions() { @@ -45,6 +45,7 @@ public class SysAdminPermissions extends AbstractPermissions { put(Resource.QUEUE, systemEntityPermissionChecker); put(Resource.NOTIFICATION, systemEntityPermissionChecker); put(Resource.MOBILE_APP_SETTINGS, PermissionChecker.allowAllPermissionChecker); + put(Resource.TRENDZ_SETTINGS, PermissionChecker.allowAllPermissionChecker); } private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index a072cf2738..35824d7858 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -56,6 +56,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.MOBILE_APP, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); put(Resource.CALCULATED_FIELD, tenantEntityPermissionChecker); + put(Resource.TRENDZ_SETTINGS, PermissionChecker.allowAllPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 418263076b..a8050b00ed 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -644,6 +644,9 @@ cache: mobileSecretKey: timeToLiveInMinutes: "${CACHE_MOBILE_SECRET_KEY_TTL:2}" # QR secret key cache TTL maxSize: "${CACHE_MOBILE_SECRET_KEY_MAX_SIZE:10000}" # 0 means the cache is disabled + trendzSettings: + timeToLiveInMinutes: "${CACHE_SPECS_TRENDZ_SETTINGS_TTL:1440}" # Trendz settings cache TTL + maxSize: "${CACHE_SPECS_TRENDZ_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled # Deliberately placed outside the 'specs' group above notificationRules: diff --git a/application/src/test/java/org/thingsboard/server/controller/TrendzControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TrendzControllerTest.java new file mode 100644 index 0000000000..8cf8d0f90c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/TrendzControllerTest.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import org.junit.Test; +import org.thingsboard.server.common.data.trendz.TrendzSettings; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class TrendzControllerTest extends AbstractControllerTest { + + private final String trendzUrl = "https://some.domain.com:18888/also_necessary_prefix"; + + @Test + public void testTrendzSettingsWhenTenant() throws Exception { + loginTenantAdmin(); + + TrendzSettings trendzSettings = doGet("/api/trendz/settings", TrendzSettings.class); + + assertThat(trendzSettings).isNotNull(); + assertThat(trendzSettings.isEnabled()).isFalse(); + assertThat(trendzSettings.getTrendzUrl()).isNull(); + + trendzSettings.setEnabled(true); + trendzSettings.setTrendzUrl(trendzUrl); + + doPost("/api/trendz/settings", trendzSettings).andExpect(status().isOk()); + + TrendzSettings updatedTrendzSettings = doGet("/api/trendz/settings", TrendzSettings.class); + assertThat(updatedTrendzSettings).isEqualTo(trendzSettings); + } + + @Test + public void testTrendzSettingsWhenCustomer() throws Exception { + loginCustomerUser(); + + TrendzSettings trendzSettings = new TrendzSettings(); + trendzSettings.setEnabled(true); + trendzSettings.setTrendzUrl("https://some.domain.com:18888/customer_trendz"); + + doPost("/api/trendz/settings", trendzSettings).andExpect(status().isForbidden()); + + TrendzSettings fetchedTrendzSettings = doGet("/api/trendz/settings", TrendzSettings.class); + assertThat(fetchedTrendzSettings).isNotNull(); + } + +} \ No newline at end of file diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/trendz/TrendzSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/trendz/TrendzSettingsService.java new file mode 100644 index 0000000000..6f66054518 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/trendz/TrendzSettingsService.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.trendz; + +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.trendz.TrendzSettings; + +public interface TrendzSettingsService { + + void saveTrendzSettings(TenantId tenantId, TrendzSettings settings); + + TrendzSettings findTrendzSettings(TenantId tenantId); + +} \ No newline at end of file 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 35f69e1544..5b167c88a2 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 @@ -35,6 +35,7 @@ public class CacheConstants { public static final String DEVICE_PROFILE_CACHE = "deviceProfiles"; public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings"; public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications"; + public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings"; public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ATTRIBUTES_CACHE = "attributes"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index b1ef4d7f22..fe3eb4e4d8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; +import org.thingsboard.server.common.data.trendz.TrendzSettings; import java.util.List; @@ -37,4 +38,5 @@ public class SystemParams { String calculatedFieldDebugPerTenantLimitsConfiguration; long maxArgumentsPerCF; long maxDataPointsPerRollingArg; + TrendzSettings trendzSettings; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/trendz/TrendzSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/trendz/TrendzSettings.java new file mode 100644 index 0000000000..dfd1967740 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/trendz/TrendzSettings.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.trendz; + +import lombok.Data; + +@Data +public class TrendzSettings { + + private boolean enabled; + private String trendzUrl; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/trendz/DefaultTrendzSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/trendz/DefaultTrendzSettingsService.java new file mode 100644 index 0000000000..98df523245 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/trendz/DefaultTrendzSettingsService.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.trendz; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.trendz.TrendzSettings; +import org.thingsboard.server.dao.settings.AdminSettingsService; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DefaultTrendzSettingsService implements TrendzSettingsService { + + private final AdminSettingsService adminSettingsService; + + private static final String SETTINGS_KEY = "trendz"; + + @CacheEvict(cacheNames = CacheConstants.TRENDZ_SETTINGS_CACHE, key = "#tenantId") + @Override + public void saveTrendzSettings(TenantId tenantId, TrendzSettings settings) { + AdminSettings adminSettings = Optional.ofNullable(adminSettingsService.findAdminSettingsByTenantIdAndKey(tenantId, SETTINGS_KEY)) + .orElseGet(() -> { + AdminSettings newAdminSettings = new AdminSettings(); + newAdminSettings.setTenantId(tenantId); + newAdminSettings.setKey(SETTINGS_KEY); + return newAdminSettings; + }); + adminSettings.setJsonValue(JacksonUtil.valueToTree(settings)); + adminSettingsService.saveAdminSettings(tenantId, adminSettings); + } + + @Cacheable(cacheNames = CacheConstants.TRENDZ_SETTINGS_CACHE, key = "#tenantId") + @Override + public TrendzSettings findTrendzSettings(TenantId tenantId) { + return Optional.ofNullable(adminSettingsService.findAdminSettingsByTenantIdAndKey(tenantId, SETTINGS_KEY)) + .map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), TrendzSettings.class)) + .orElseGet(TrendzSettings::new); + } + +}