From d0ff4a2d869d2cb91015ce91e1e7b16ad4252c50 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 18 Apr 2024 11:25:05 +0300 Subject: [PATCH] mobile app QR code initial implementation --- .../main/data/upgrade/3.6.4/schema_update.sql | 12 +++ .../server/controller/AdminController.java | 31 ++++++ .../controller/MobileAppLinksController.java | 86 +++++++++++++++++ .../server/controller/QRCodeController.java | 94 +++++++++++++++++++ .../service/qr/QRSecretCaffeineCache.java | 33 +++++++ .../server/service/qr/QRSecretEvictEvent.java | 25 +++++ .../server/service/qr/QRSecretRedisCache.java | 36 +++++++ .../server/service/qr/QRService.java | 28 ++++++ .../server/service/qr/QRServiceImpl.java | 66 +++++++++++++ .../service/security/permission/Resource.java | 4 +- .../permission/SysAdminPermissions.java | 1 + .../permission/TenantAdminPermissions.java | 1 + .../system/DefaultSystemSecurityService.java | 4 +- .../src/main/resources/thingsboard.yml | 6 ++ .../controller/AdminControllerTest.java | 24 +++++ .../resources/application-test.properties | 1 + .../server/common/data/CacheConstants.java | 2 + .../common/data/mobile/MobileAppSettings.java | 63 +++++++++++++ .../common/data/security/model/JwtPair.java | 4 +- .../data/security/model/SecuritySettings.java | 2 + .../mobile/BaseMobileAppSettingsService.java | 92 ++++++++++++++++++ .../MobileAppSettingsCaffeineCache.java | 34 +++++++ .../dao/mobile/MobileAppSettingsDao.java | 29 ++++++ .../mobile/MobileAppSettingsEvictEvent.java | 24 +++++ .../mobile/MobileAppSettingsRedisCache.java | 36 +++++++ .../dao/mobile/MobileAppSettingsService.java | 27 ++++++ .../server/dao/model/ModelConstants.java | 9 ++ .../model/sql/MobileAppSettingsEntity.java | 92 ++++++++++++++++++ .../dao/sql/mobile/JpaMobileAppDao.java | 53 +++++++++++ .../mobile/MobileAppSettingsRepository.java | 34 +++++++ .../main/resources/sql/schema-entities.sql | 8 ++ 31 files changed, 957 insertions(+), 4 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/controller/MobileAppLinksController.java create mode 100644 application/src/main/java/org/thingsboard/server/controller/QRCodeController.java create mode 100644 application/src/main/java/org/thingsboard/server/service/qr/QRSecretCaffeineCache.java create mode 100644 application/src/main/java/org/thingsboard/server/service/qr/QRSecretEvictEvent.java create mode 100644 application/src/main/java/org/thingsboard/server/service/qr/QRSecretRedisCache.java create mode 100644 application/src/main/java/org/thingsboard/server/service/qr/QRService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/qr/QRServiceImpl.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java diff --git a/application/src/main/data/upgrade/3.6.4/schema_update.sql b/application/src/main/data/upgrade/3.6.4/schema_update.sql index d7d8887d13..99c503d8f9 100644 --- a/application/src/main/data/upgrade/3.6.4/schema_update.sql +++ b/application/src/main/data/upgrade/3.6.4/schema_update.sql @@ -134,3 +134,15 @@ DELETE FROM asset WHERE type='TbServiceQueue'; DELETE FROM asset_profile WHERE name ='TbServiceQueue'; -- QUEUE STATS UPDATE END + +-- MOBILE APPS TABLE CREATE START + +CREATE TABLE IF NOT EXISTS mobile_app_settings ( + tenant_id UUID NOT NULL, + app_package VARCHAR(100) UNIQUE, + sha256_cert_fingerprints VARCHAR(10000), + app_id VARCHAR(100) UNIQUE, + settings VARCHAR(100000) +); + +-- MOBILE APPS TABLE CREATE END \ No newline at end of file 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 818a2a15a6..2a6593f452 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -65,6 +65,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; 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.mobile.MobileAppSettings; 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; @@ -75,6 +76,7 @@ import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo; import org.thingsboard.server.common.data.sync.vc.VcUtils; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.audit.AuditLogService; +import org.thingsboard.server.dao.mobile.MobileAppSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; @@ -95,6 +97,7 @@ import java.util.Optional; import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; @RestController @TbCoreComponent @@ -119,6 +122,7 @@ public class AdminController extends BaseController { private final UpdateService updateService; private final SystemInfoService systemInfoService; private final AuditLogService auditLogService; + private final MobileAppSettingsService mobileAppSettingsService; @ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)", notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @@ -482,4 +486,31 @@ public class AdminController extends BaseController { adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings); response.sendRedirect(prevUri); } + + @ApiOperation(value = "Create Or Update the Mobile application settings (saveMobileAppSettings)", + notes = "The payload contains platform qr code widget settings and associated android and iOS applications." + SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/mobileAppSettings", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public MobileAppSettings saveMobileAppSettings(@Parameter(description = "A JSON value representing the mobile apps configuration") + @RequestBody MobileAppSettings mobileAppSettings) throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.WRITE); + mobileAppSettings.setTenantId(getTenantId()); + + return mobileAppSettingsService.saveMobileAppSettings(currentUser.getTenantId(), mobileAppSettings); + } + + @ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)", + notes = "The payload contains platform qr code widget settings and creds for associated android and iOS applications." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/mobileAppSettings", method = RequestMethod.GET) + @ResponseBody + public MobileAppSettings getMobileAppSettings() throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ); + + return mobileAppSettingsService.getMobileAppSettings(currentUser.getTenantId()); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppLinksController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppLinksController.java new file mode 100644 index 0000000000..1265715126 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppLinksController.java @@ -0,0 +1,86 @@ +/** + * Copyright © 2016-2024 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 com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.config.annotations.ApiOperation; +import org.thingsboard.server.dao.mobile.MobileAppSettingsService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@RequiredArgsConstructor +@RestController +@TbCoreComponent +public class MobileAppLinksController extends BaseController { + + public static final String ASSET_LINKS_PATTERN = "[{\n" + + " \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n" + + " \"target\": {\n" + + " \"namespace\": \"android_app\",\n" + + " \"package_name\": \"%s\",\n" + + " \"sha256_cert_fingerprints\":\n" + + " [\"%s\"]\n" + + " }\n" + + "}]"; + + public static final String APPLE_APP_SITE_ASSOCIATION_PATTERN = "{\n" + + " \"applinks\": {\n" + + " \"apps\": [],\n" + + " \"details\": [\n" + + " {\n" + + " \"appID\": \"%s\",\n" + + " \"paths\": [ \"*\" ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + + + private final MobileAppSettingsService mobileAppSettingsService; + + + @ApiOperation(value = "Get associated android applications (getAssetLinks)") + @RequestMapping(value = "/.well-known/assetlinks.json", method = RequestMethod.GET) + @ResponseBody + public ResponseEntity getAssetLinks() { + MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); + if (mobileAppSettings != null && mobileAppSettings.getAppPackage() != null && mobileAppSettings.getSha256CertFingerprints() != null) { + return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, mobileAppSettings.getAppPackage(), mobileAppSettings.getSha256CertFingerprints()))); + } else { + return ResponseEntity.notFound().build(); + } + } + + @ApiOperation(value = "Get associated ios applications (getAppleAppSiteAssociation)") + @RequestMapping(value = "/.well-known/apple-app-site-association", method = RequestMethod.GET) + @ResponseBody + public ResponseEntity getAppleAppSiteAssociation() { + MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); + if (mobileAppSettings != null && mobileAppSettings.getAppId() != null) { + return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, mobileAppSettings.getAppId()))); + } else { + return ResponseEntity.notFound().build(); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/QRCodeController.java b/application/src/main/java/org/thingsboard/server/controller/QRCodeController.java new file mode 100644 index 0000000000..aed35cb153 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/QRCodeController.java @@ -0,0 +1,94 @@ +/** + * Copyright © 2016-2024 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 com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.exception.ThingsboardException; +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.security.model.JwtPair; +import org.thingsboard.server.config.annotations.ApiOperation; +import org.thingsboard.server.dao.mobile.MobileAppSettingsService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.qr.QRService; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.system.SystemSecurityService; + +import java.net.URI; +import java.net.URISyntaxException; + +@RequiredArgsConstructor +@RestController +@TbCoreComponent +@RequestMapping("/api") +public class QRCodeController extends BaseController { + + public static final String SECRET = "secret"; + public static final String SECRET_PARAM_DESCRIPTION = "A string value representing short-live secret key"; + public static final String DEFAULT_APP_DOMAIN = "demo.thingsboard.io"; + public static final String DEEP_LINK_PATTERN = "https://%s/api/noauth/qr?secret=%s"; + private final QRService qrService; + private final SystemSecurityService systemSecurityService; + + private final MobileAppSettingsService mobileAppSettingsService; + + @ApiOperation(value = "Get the deep link to the associated mobile application (getQRCodeDeepLink)", + notes = "Fetch the url that takes user to associated mobile application ") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/qr/deepLink", method = RequestMethod.GET, produces = "text/plain") + @ResponseBody + public String getQRCodeDeepLink(HttpServletRequest request) throws ThingsboardException, URISyntaxException { + SecurityUser currentUser = getCurrentUser(); + String secret = qrService.generateSecret(currentUser); + + String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request); + String platformDomain = new URI(baseUrl).getHost(); + JsonNode mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID).getSettings(); + String appDomain; + if (mobileAppSettings != null && mobileAppSettings.get("useDefault") != null + && !mobileAppSettings.get("useDefault").asBoolean()) { + appDomain = platformDomain; + } else { + appDomain = DEFAULT_APP_DOMAIN; + } + String deepLink = String.format(DEEP_LINK_PATTERN, appDomain, secret); + if (!appDomain.equals(platformDomain)) { + deepLink = deepLink + "&host=" + baseUrl; + } + return deepLink; + } + + @ApiOperation(value = "Get User Token (getUserToken)", + notes = "Returns the token of the User based on the provided secret key.") + @RequestMapping(value = "/noauth/qr/{secret}", method = RequestMethod.GET) + @ResponseBody + public JwtPair getUserToken(@Parameter(description = SECRET_PARAM_DESCRIPTION) + @PathVariable(SECRET) String secret) throws ThingsboardException { + checkParameter(SECRET, secret); + return qrService.getJwtPair(secret); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/qr/QRSecretCaffeineCache.java b/application/src/main/java/org/thingsboard/server/service/qr/QRSecretCaffeineCache.java new file mode 100644 index 0000000000..d74307926c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/qr/QRSecretCaffeineCache.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2024 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.qr; + +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.security.model.JwtPair; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("QRSecretCache") +public class QRSecretCaffeineCache extends CaffeineTbTransactionalCache { + + public QRSecretCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.QR_SECRET_KEY_CACHE); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/qr/QRSecretEvictEvent.java b/application/src/main/java/org/thingsboard/server/service/qr/QRSecretEvictEvent.java new file mode 100644 index 0000000000..bf0c7b67fe --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/qr/QRSecretEvictEvent.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2024 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.qr; + +import lombok.Data; + +@Data +public class QRSecretEvictEvent { + + private final String secret; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/qr/QRSecretRedisCache.java b/application/src/main/java/org/thingsboard/server/service/qr/QRSecretRedisCache.java new file mode 100644 index 0000000000..8efe664187 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/qr/QRSecretRedisCache.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2024 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.qr; + +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.TbJsonRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.model.JwtPair; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("QRSecretCache") +public class QRSecretRedisCache extends RedisTbTransactionalCache { + + public QRSecretRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.QR_SECRET_KEY_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(JwtPair.class)); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/qr/QRService.java b/application/src/main/java/org/thingsboard/server/service/qr/QRService.java new file mode 100644 index 0000000000..9502d3440b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/qr/QRService.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2024 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.qr; + +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.security.model.JwtPair; +import org.thingsboard.server.service.security.model.SecurityUser; + +public interface QRService { + + String generateSecret(SecurityUser securityUser); + + JwtPair getJwtPair(String secret) throws ThingsboardException; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/qr/QRServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/qr/QRServiceImpl.java new file mode 100644 index 0000000000..36bf72c74e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/qr/QRServiceImpl.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2024 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.qr; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.server.cache.TbCacheValueWrapper; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.security.model.JwtPair; +import org.thingsboard.server.dao.entity.AbstractCachedService; +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; + +import static org.thingsboard.server.service.security.system.DefaultSystemSecurityService.DEFAULT_QR_SECRET_KEY_LENGTH; + +@Service +@Slf4j +@RequiredArgsConstructor +public class QRServiceImpl extends AbstractCachedService implements QRService { + + private final JwtTokenFactory tokenFactory; + private final SystemSecurityService systemSecurityService; + + @Override + public String generateSecret(SecurityUser securityUser) { + log.trace("Executing generateSecret for user [{}]", securityUser.getId()); + Integer qrSecretKeyLength = systemSecurityService.getSecuritySettings().getQrSecretKeyLength(); + String secret = StringUtils.generateSafeToken(qrSecretKeyLength == null ? DEFAULT_QR_SECRET_KEY_LENGTH : qrSecretKeyLength); + cache.put(secret, tokenFactory.createTokenPair(securityUser)); + return secret; + } + + @Override + public JwtPair getJwtPair(String secret) throws ThingsboardException { + TbCacheValueWrapper jwtPair = cache.get(secret); + if (jwtPair != null) { + return jwtPair.get(); + } else { + throw new ThingsboardException("Jwt token not found or expired!", ThingsboardErrorCode.JWT_TOKEN_EXPIRED); + } + } + + @TransactionalEventListener(classes = QRSecretEvictEvent.class) + @Override + public void handleEvictEvent(QRSecretEvictEvent event) { + cache.evict(event.getSecret()); + } +} 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 5aa869f2a8..d43663fb97 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 @@ -46,8 +46,8 @@ public enum Resource { QUEUE(EntityType.QUEUE), VERSION_CONTROL, NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE, - EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE); - + EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), + MOBILE_APP_SETTINGS; private final Set entityTypes; Resource() { 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 67cc105c91..d906938e47 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 @@ -41,6 +41,7 @@ public class SysAdminPermissions extends AbstractPermissions { put(Resource.TB_RESOURCE, systemEntityPermissionChecker); put(Resource.QUEUE, systemEntityPermissionChecker); put(Resource.NOTIFICATION, systemEntityPermissionChecker); + put(Resource.MOBILE_APP_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 bfe0af966f..f4117e1a36 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 @@ -50,6 +50,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.QUEUE, queuePermissionChecker); put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker); put(Resource.NOTIFICATION, tenantEntityPermissionChecker); + put(Resource.MOBILE_APP_SETTINGS, PermissionChecker.allowAllPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { 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 d3f945cda1..a804565f59 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 @@ -57,7 +57,6 @@ import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserServiceImpl; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; -import org.thingsboard.server.service.security.exception.UserPasswordNotValidException; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.utils.MiscUtils; import ua_parser.Client; @@ -75,6 +74,8 @@ import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTING @Slf4j public class DefaultSystemSecurityService implements SystemSecurityService { + public static final int DEFAULT_QR_SECRET_KEY_LENGTH = 64; + @Autowired private AdminSettingsService adminSettingsService; @@ -109,6 +110,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { securitySettings.setPasswordPolicy(new UserPasswordPolicy()); securitySettings.getPasswordPolicy().setMinimumLength(6); securitySettings.getPasswordPolicy().setMaximumLength(72); + securitySettings.setQrSecretKeyLength(DEFAULT_QR_SECRET_KEY_LENGTH); } return securitySettings; } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 33146b1b82..2a0c8693ad 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -579,6 +579,12 @@ cache: alarmTypes: timeToLiveInMinutes: "${CACHE_SPECS_ALARM_TYPES_TTL:60}" # Alarm types cache TTL maxSize: "${CACHE_SPECS_ALARM_TYPES_MAX_SIZE:10000}" # 0 means the cache is disabled + mobileAppSettings: + timeToLiveInMinutes: "${CACHE_SPECS_MOBILE_APP_SETTINGS_TTL:1440}" # Mobile application cache TTL + maxSize: "${CACHE_SPECS_MOBILE_APP_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled + qrSecretKey: + timeToLiveInMinutes: "${CACHE_QR_SECRET_KEY_TTL:2}" # QR secret key cache TTL + maxSize: "${CACHE_QR_SECRET_KEY_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/AdminControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java index 430ff2eb09..2d1ae9fdbb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.mockito.Mockito; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -181,4 +182,27 @@ public class AdminControllerTest extends AbstractControllerTest { resetJwtSettingsToDefault(); } + @Test + public void testSaveMobileAppSettings() throws Exception { + loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/admin/mobileAppSettings", MobileAppSettings.class); + assertThat(mobileAppSettings.getSettings().get("qrSecretKeyRefreshRateInMin").asInt()).isEqualTo(1); + + JsonNode jsonValue = mobileAppSettings.getSettings(); + ((ObjectNode) jsonValue).put("useDefault", false); + + doPost("/api/admin/mobileAppSettings", mobileAppSettings) + .andExpect(status().isOk()); + + doGet("/api/admin/mobileAppSettings") + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.settings.useDefault", is(false))); + + // check refresh rate should be less than secret ttl (5 min) + ((ObjectNode) jsonValue).put("qrSecretKeyRefreshRateInMin", 6); + doPost("/api/admin/mobileAppSettings", mobileAppSettings) + .andExpect(status().isBadRequest()); + } + } diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 92959e4552..2e5944e0dc 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -8,6 +8,7 @@ transport.lwm2m.bootstrap.security.credentials.keystore.store_file=lwm2m/credent transport.lwm2m.security.trust-credentials.enabled=true transport.lwm2m.security.trust-credentials.type=KEYSTORE transport.lwm2m.security.trust-credentials.keystore.store_file=lwm2m/credentials/lwm2mtruststorechain.jks +cache.specs.qrSecretKey.timeToLiveInMinutes=5 # Edge disabled to speed up the context init. Will be enabled by @TestPropertySource in respective tests edges.enabled=false 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 9fda1c6bb5..8eb8cd8678 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 @@ -46,4 +46,6 @@ public class CacheConstants { public static final String ENTITY_COUNT_CACHE = "entityCount"; public static final String RESOURCE_INFO_CACHE = "resourceInfo"; public static final String ALARM_TYPES_CACHE = "alarmTypes"; + public static final String MOBILE_APP_SETTINGS_CACHE = "mobileAppSettings"; + public static final String QR_SECRET_KEY_CACHE = "qrSecretKey"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java new file mode 100644 index 0000000000..039143f4a9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2024 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.mobile; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +import java.io.Serializable; + +import static org.thingsboard.server.common.data.BaseDataWithAdditionalInfo.getJson; +import static org.thingsboard.server.common.data.BaseDataWithAdditionalInfo.setJson; + +@Data +public class MobileAppSettings implements Serializable { + + private static final long serialVersionUID = 2628323657987010348L; + + private TenantId tenantId; + + @NoXss + @Length(fieldName = "appPackage") + private String appPackage; + + @NoXss + @Length(fieldName = "sha256CertFingerprints") + private String sha256CertFingerprints; + + @NoXss + @Length(fieldName = "appId") + private String appId; + + @NoXss + @Length(fieldName = "settings", max = 10000000) + private transient JsonNode settings; + + @JsonIgnore + private byte[] settingsBytes; + + public JsonNode getSettings() { + return getJson(() -> settings, () -> settingsBytes); + } + + public void setSettings(JsonNode settings) { + setJson(settings, json -> this.settings = json, bytes -> this.settingsBytes = bytes); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java index 459aa7b26f..ae484136ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java @@ -20,10 +20,12 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.security.Authority; +import java.io.Serializable; + @Schema(description = "JWT Pair") @Data @NoArgsConstructor -public class JwtPair { +public class JwtPair implements Serializable { @Schema(description = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..") private String token; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java index 07262c7eb4..b6ee1a17a5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java @@ -32,4 +32,6 @@ public class SecuritySettings implements Serializable { private Integer maxFailedLoginAttempts; @Schema(description = "Email to use for notifications about locked users." ) private String userLockoutNotificationEmail; + @Schema(description = "QR secret key length" ) + private Integer qrSecretKeyLength; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java new file mode 100644 index 0000000000..fa035cc32d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2024 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.mobile; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.dao.entity.AbstractCachedService; +import org.thingsboard.server.dao.exception.DataValidationException; + +@Service +@Slf4j +@RequiredArgsConstructor +public class BaseMobileAppSettingsService extends AbstractCachedService implements MobileAppSettingsService { + + @Value("${cache.specs.qrSecretKey.timeToLiveInMinutes:2}") + private int secretKeyTtl; + + private static final int MIN_TIME_TO_DOWNLOAD_MOBILE_APP_IN_MIN = 1; + private final MobileAppSettingsDao mobileAppSettingsDao; + + @Override + public MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings settings) { + if (settings.getSettings() != null && settings.getSettings().get("qrSecretKeyRefreshRateInMin") != null + && (settings.getSettings().get("qrSecretKeyRefreshRateInMin").asInt() + MIN_TIME_TO_DOWNLOAD_MOBILE_APP_IN_MIN > secretKeyTtl)) { + throw new DataValidationException("Refresh rate should be less than server secret key ttl"); + } + MobileAppSettings mobileAppSettings = mobileAppSettingsDao.save(tenantId, settings); + publishEvictEvent(new MobileAppSettingsEvictEvent(tenantId)); + return mobileAppSettings; + } + + @Override + public MobileAppSettings getMobileAppSettings(TenantId tenantId) { + log.trace("Executing getMobileAppSettings for tenant [{}] ", tenantId); + MobileAppSettings mobileAppSettings = cache.getAndPutInTransaction(tenantId, + () -> mobileAppSettingsDao.findByTenantId(tenantId), false); + return constructMobileAppSettings(mobileAppSettings); + } + + @TransactionalEventListener(classes = MobileAppSettingsEvictEvent.class) + @Override + public void handleEvictEvent(MobileAppSettingsEvictEvent event) { + cache.evict(event.getTenantId()); + } + + private MobileAppSettings constructMobileAppSettings(MobileAppSettings mobileAppSettings) { + if (mobileAppSettings == null) { + mobileAppSettings = new MobileAppSettings(); + + ObjectNode settings = JacksonUtil.newObjectNode(); + settings.put("useDefault", true); + settings.put("showOnHomePage", true); + settings.put("qrLabel", "Scan to connect or download mobile app"); + settings.put("qrSecretKeyRefreshRateInMin", 1); + + ObjectNode androidSettings = JacksonUtil.newObjectNode(); + androidSettings.put("badge", "original"); + androidSettings.put("badgePosition", "Right"); + settings.set("android", androidSettings); + + ObjectNode iOSSettings = JacksonUtil.newObjectNode(); + iOSSettings.put("badge", "original"); + iOSSettings.put("badgePosition", "Right"); + settings.set("iOS", iOSSettings); + + mobileAppSettings.setSettings(settings); + } + return mobileAppSettings; + } + + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java new file mode 100644 index 0000000000..9e15ae8572 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2024 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.mobile; + +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.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("MobileAppCache") +public class MobileAppSettingsCaffeineCache extends CaffeineTbTransactionalCache { + + public MobileAppSettingsCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.MOBILE_APP_SETTINGS_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java new file mode 100644 index 0000000000..ea8b4c3b3c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2024 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.mobile; + +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; + + +public interface MobileAppSettingsDao { + + MobileAppSettings save(TenantId tenantId, MobileAppSettings whiteLabeling); + + MobileAppSettings findByTenantId(TenantId tenantId); + + void removeByTenantId(TenantId tenantId); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java new file mode 100644 index 0000000000..459e61ef6d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2024 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.mobile; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class MobileAppSettingsEvictEvent { + private final TenantId tenantId; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java new file mode 100644 index 0000000000..9dd149abcb --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java @@ -0,0 +1,36 @@ + /** + * Copyright © 2016-2024 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.mobile; + +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.TbJsonRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("MobileAppCache") +public class MobileAppSettingsRedisCache extends RedisTbTransactionalCache { + + public MobileAppSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.MOBILE_APP_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(MobileAppSettings.class)); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java new file mode 100644 index 0000000000..b170e4ab91 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2024 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.mobile; + +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; + +public interface MobileAppSettingsService { + + MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings settings); + + MobileAppSettings getMobileAppSettings(TenantId tenantId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index ae15c2d029..f7f061e37a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -659,6 +659,15 @@ public class ModelConstants { public static final String NOTIFICATION_TEMPLATE_NOTIFICATION_TYPE_PROPERTY = "notification_type"; public static final String NOTIFICATION_TEMPLATE_CONFIGURATION_PROPERTY = "configuration"; + /** + * Mobile application settings constants. + */ + public static final String MOBILE_APP_SETTINGS_TABLE_NAME = "mobile_app_settings"; + public static final String MOBILE_APP_SETTINGS_APP_PACKAGE_PROPERTY = "app_package"; + public static final String MOBILE_APP_SETTINGS_SHA256_CERT_FINGERPRINTS_PROPERTY = "sha256_cert_fingerprints"; + public static final String MOBILE_APP_APP_ID_PROPERTY = "app_id"; + public static final String MOBILE_APP_SETTINGS_PROPERTY = "settings"; + protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN), max(TS_COLUMN)}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java new file mode 100644 index 0000000000..faceffdca8 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2024 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 com.fasterxml.jackson.databind.JsonNode; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.ToData; +import org.thingsboard.server.dao.util.mapping.JsonConverter; + +import java.io.Serializable; +import java.util.UUID; + +@Data +@NoArgsConstructor +@Entity +@Table(name = ModelConstants.MOBILE_APP_SETTINGS_TABLE_NAME) +public class MobileAppSettingsEntity implements ToData, Serializable { + + @Id + @Column(name = ModelConstants.TENANT_ID_COLUMN, columnDefinition = "uuid") + protected UUID tenantId; + + @Column(name = ModelConstants.MOBILE_APP_SETTINGS_APP_PACKAGE_PROPERTY) + private String appPackage; + + @Column(name = ModelConstants.MOBILE_APP_SETTINGS_SHA256_CERT_FINGERPRINTS_PROPERTY) + private String sha256CertFingerprints; + + @Column(name = ModelConstants.MOBILE_APP_APP_ID_PROPERTY) + private String appId; + + @Convert(converter = JsonConverter.class) + @Column(name = ModelConstants.MOBILE_APP_SETTINGS_PROPERTY) + private JsonNode settings; + + public MobileAppSettingsEntity(MobileAppSettings mobileAppSettings) { + this.tenantId = mobileAppSettings.getTenantId().getId(); + if (mobileAppSettings.getAppPackage() != null) { + this.appPackage = mobileAppSettings.getAppPackage(); + } + if (mobileAppSettings.getSha256CertFingerprints() != null) { + this.sha256CertFingerprints = mobileAppSettings.getSha256CertFingerprints(); + } + if (mobileAppSettings.getAppId() != null) { + this.appId = mobileAppSettings.getAppId(); + } + if (mobileAppSettings.getSettings() != null) { + this.settings = mobileAppSettings.getSettings(); + } + } + + @Override + public MobileAppSettings toData() { + MobileAppSettings mobileAppSettings = new MobileAppSettings(); + mobileAppSettings.setTenantId(TenantId.fromUUID(tenantId)); + if (appPackage != null) { + mobileAppSettings.setAppPackage(appPackage); + } + if (sha256CertFingerprints != null) { + mobileAppSettings.setSha256CertFingerprints(sha256CertFingerprints); + } + if (appId != null) { + mobileAppSettings.setAppId(appId); + } + if (settings != null) { + mobileAppSettings.setSettings(settings); + } + return mobileAppSettings; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java new file mode 100644 index 0000000000..f445cb0829 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2024 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.sql.mobile; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.mobile.MobileAppSettingsDao; +import org.thingsboard.server.dao.model.sql.MobileAppSettingsEntity; +import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; +import org.thingsboard.server.dao.util.SqlDao; + + +@Component +@Slf4j +@SqlDao +public class JpaMobileAppDao extends JpaAbstractDaoListeningExecutorService implements MobileAppSettingsDao { + + @Autowired + private MobileAppSettingsRepository mobileAppSettingsRepository; + + @Override + public MobileAppSettings save(TenantId tenantId, MobileAppSettings whiteLabeling) { + return DaoUtil.getData(mobileAppSettingsRepository.save(new MobileAppSettingsEntity(whiteLabeling))); + } + + @Override + public MobileAppSettings findByTenantId(TenantId tenantId) { + return DaoUtil.getData(mobileAppSettingsRepository.findById(tenantId.getId())); + } + + @Override + public void removeByTenantId(TenantId tenantId) { + mobileAppSettingsRepository.deleteByTenantId(tenantId.getId()); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java new file mode 100644 index 0000000000..a2339e1354 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2024 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.sql.mobile; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sql.MobileAppSettingsEntity; + +import java.util.UUID; + + +public interface MobileAppSettingsRepository extends JpaRepository { + + @Transactional + @Modifying + @Query("DELETE FROM MobileAppSettingsEntity r WHERE r.tenantId = :tenantId") + void deleteByTenantId(@Param("tenantId") UUID tenantId); +} diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 1199f45181..cc6b950be9 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -893,4 +893,12 @@ CREATE TABLE IF NOT EXISTS queue_stats ( queue_name varchar(255) NOT NULL, service_id varchar(255) NOT NULL, CONSTRAINT queue_stats_name_unq_key UNIQUE (tenant_id, queue_name, service_id) +); + +CREATE TABLE IF NOT EXISTS mobile_app_settings ( + tenant_id UUID NOT NULL, + app_package VARCHAR(100) UNIQUE, + sha256_cert_fingerprints VARCHAR(10000), + app_id VARCHAR(100) UNIQUE, + settings VARCHAR(100000) ); \ No newline at end of file