From d0ff4a2d869d2cb91015ce91e1e7b16ad4252c50 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 18 Apr 2024 11:25:05 +0300 Subject: [PATCH 01/13] 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 From 67ba6441952a7a25eb4299725da718d38404b7fe Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 24 Apr 2024 19:02:14 +0300 Subject: [PATCH 02/13] moved all apis to one controller --- .../main/data/upgrade/3.6.4/schema_update.sql | 8 +- .../server/controller/AdminController.java | 31 ---- .../controller/MobileAppLinksController.java | 86 --------- .../MobileApplicationController.java | 173 ++++++++++++++++++ .../server/controller/QRCodeController.java | 94 ---------- ...rvice.java => MobileAppSecretService.java} | 4 +- ...l.java => MobileAppSecretServiceImpl.java} | 14 +- ...he.java => MobileSecretCaffeineCache.java} | 6 +- ...Event.java => MobileSecretEvictEvent.java} | 2 +- ...Cache.java => MobileSecretRedisCache.java} | 6 +- .../system/DefaultSystemSecurityService.java | 4 +- .../src/main/resources/thingsboard.yml | 6 +- .../controller/AdminControllerTest.java | 24 --- .../MobileApplicationControllerTest.java | 132 +++++++++++++ .../resources/application-test.properties | 1 - .../server/common/data/CacheConstants.java | 2 +- .../common/data/mobile/AndroidConfig.java | 38 ++++ .../common/data/mobile/BadgePosition.java | 23 +++ .../server/common/data/mobile/BadgeStyle.java | 24 +++ .../server/common/data/mobile/IosConfig.java | 36 ++++ .../common/data/mobile/MobileAppSettings.java | 41 +---- .../common/data/mobile/QRCodeConfig.java | 38 ++++ .../data/security/model/SecuritySettings.java | 4 +- .../mobile/BaseMobileAppSettingsService.java | 43 ++--- .../dao/mobile/MobileAppSettingsDao.java | 2 +- .../server/dao/model/ModelConstants.java | 8 +- .../model/sql/MobileAppSettingsEntity.java | 54 +++--- .../dao/sql/mobile/JpaMobileAppDao.java | 4 +- .../main/resources/sql/schema-entities.sql | 8 +- 29 files changed, 552 insertions(+), 364 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/controller/MobileAppLinksController.java create mode 100644 application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java delete mode 100644 application/src/main/java/org/thingsboard/server/controller/QRCodeController.java rename application/src/main/java/org/thingsboard/server/service/qr/{QRService.java => MobileAppSecretService.java} (89%) rename application/src/main/java/org/thingsboard/server/service/qr/{QRServiceImpl.java => MobileAppSecretServiceImpl.java} (77%) rename application/src/main/java/org/thingsboard/server/service/qr/{QRSecretCaffeineCache.java => MobileSecretCaffeineCache.java} (83%) rename application/src/main/java/org/thingsboard/server/service/qr/{QRSecretEvictEvent.java => MobileSecretEvictEvent.java} (95%) rename application/src/main/java/org/thingsboard/server/service/qr/{QRSecretRedisCache.java => MobileSecretRedisCache.java} (78%) create mode 100644 application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgeStyle.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.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 99c503d8f9..b7bbdc761f 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 @@ -139,10 +139,10 @@ DELETE FROM asset_profile WHERE name ='TbServiceQueue'; 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) + use_default boolean, + android_config VARCHAR(1000), + ios_config VARCHAR(1000), + qr_code_config 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 2a6593f452..818a2a15a6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -65,7 +65,6 @@ 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; @@ -76,7 +75,6 @@ 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; @@ -97,7 +95,6 @@ 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 @@ -122,7 +119,6 @@ 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) @@ -486,31 +482,4 @@ 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 deleted file mode 100644 index 1265715126..0000000000 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppLinksController.java +++ /dev/null @@ -1,86 +0,0 @@ -/** - * 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/MobileApplicationController.java b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java new file mode 100644 index 0000000000..a65d8506ff --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java @@ -0,0 +1,173 @@ +/** + * 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.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; +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.AndroidConfig; +import org.thingsboard.server.common.data.mobile.IosConfig; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; +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.MobileAppSecretService; +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 org.thingsboard.server.service.security.system.SystemSecurityService; + +import java.net.URI; +import java.net.URISyntaxException; + +import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; + +@RequiredArgsConstructor +@RestController +@TbCoreComponent +public class MobileApplicationController extends BaseController { + + @Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}") + private int mobileSecretKeyTtl; + + 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\": [ \"/api/noauth/qr\" ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + + 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&ttl=%s"; + private final SystemSecurityService systemSecurityService; + private final MobileAppSecretService mobileAppSecretService; + private final MobileAppSettingsService mobileAppSettingsService; + + @ApiOperation(value = "Get associated android applications (getAssetLinks)") + @GetMapping(value = "/.well-known/assetlinks.json") + public ResponseEntity getAssetLinks() { + MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); + AndroidConfig androidConfig = mobileAppSettings.getAndroidConfig(); + if (androidConfig != null && androidConfig.isEnabled() && !androidConfig.getAppPackage().isBlank() && !androidConfig.getSha256CertFingerprints().isBlank()) { + return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, androidConfig.getAppPackage(), androidConfig.getSha256CertFingerprints()))); + } else { + return ResponseEntity.notFound().build(); + } + } + + @ApiOperation(value = "Get associated ios applications (getAppleAppSiteAssociation)") + @GetMapping(value = "/.well-known/apple-app-site-association") + public ResponseEntity getAppleAppSiteAssociation() { + MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); + IosConfig iosConfig = mobileAppSettings.getIosConfig(); + + if (iosConfig != null && iosConfig.isEnabled() && !iosConfig.getAppId().isBlank()) { + return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, iosConfig.getAppId()))); + } else { + return ResponseEntity.notFound().build(); + } + } + + @ApiOperation(value = "Create Or Update the Mobile application settings (saveMobileAppSettings)", + notes = "The payload contains associated android and iOS applications and platform qr code widget settings." + SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @PostMapping(value = "/api/mobile/app/settings") + 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 associated android and iOS applications and platform qr code widget settings." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/api/mobile/app/settings") + public MobileAppSettings getMobileAppSettings() throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ); + + return mobileAppSettingsService.getMobileAppSettings(currentUser.getTenantId()); + } + + @ApiOperation(value = "Get the deep link to the associated mobile application (getMobileAppDeepLink)", + notes = "Fetch the url that takes user to associated mobile application " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/api/mobile/deepLink", produces = "text/plain") + public String getMobileAppDeepLink(HttpServletRequest request) throws ThingsboardException, URISyntaxException { + SecurityUser currentUser = getCurrentUser(); + String secret = mobileAppSecretService.generateMobileAppSecret(currentUser); + + String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request); + String platformDomain = new URI(baseUrl).getHost(); + MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); + String appDomain; + if (!mobileAppSettings.isUseDefault()) { + appDomain = platformDomain; + } else { + appDomain = DEFAULT_APP_DOMAIN; + } + String deepLink = String.format(DEEP_LINK_PATTERN, appDomain, secret, mobileSecretKeyTtl); + if (!appDomain.equals(platformDomain)) { + deepLink = deepLink + "&host=" + baseUrl; + } + return deepLink; + } + + @ApiOperation(value = "Get User Token (getUserTokenByMobileSecret)", + notes = "Returns the token of the User based on the provided secret key.") + @GetMapping(value = "/api/noauth/qr/{secret}") + public JwtPair getUserTokenByMobileSecret(@Parameter(description = SECRET_PARAM_DESCRIPTION) + @PathVariable(SECRET) String secret) throws ThingsboardException { + checkParameter(SECRET, secret); + return mobileAppSecretService.getJwtPair(secret); + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/QRCodeController.java b/application/src/main/java/org/thingsboard/server/controller/QRCodeController.java deleted file mode 100644 index aed35cb153..0000000000 --- a/application/src/main/java/org/thingsboard/server/controller/QRCodeController.java +++ /dev/null @@ -1,94 +0,0 @@ -/** - * 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/QRService.java b/application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretService.java similarity index 89% rename from application/src/main/java/org/thingsboard/server/service/qr/QRService.java rename to application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretService.java index 9502d3440b..943fd45c54 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/QRService.java +++ b/application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretService.java @@ -19,9 +19,9 @@ 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 { +public interface MobileAppSecretService { - String generateSecret(SecurityUser securityUser); + String generateMobileAppSecret(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/MobileAppSecretServiceImpl.java similarity index 77% rename from application/src/main/java/org/thingsboard/server/service/qr/QRServiceImpl.java rename to application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretServiceImpl.java index 36bf72c74e..f8d8629d10 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/QRServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretServiceImpl.java @@ -29,21 +29,21 @@ 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; +import static org.thingsboard.server.service.security.system.DefaultSystemSecurityService.DEFAULT_MOBILE_SECRET_KEY_LENGTH; @Service @Slf4j @RequiredArgsConstructor -public class QRServiceImpl extends AbstractCachedService implements QRService { +public class MobileAppSecretServiceImpl extends AbstractCachedService implements MobileAppSecretService { private final JwtTokenFactory tokenFactory; private final SystemSecurityService systemSecurityService; @Override - public String generateSecret(SecurityUser securityUser) { + public String generateMobileAppSecret(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); + Integer mobileSecretKeyLength = systemSecurityService.getSecuritySettings().getMobileSecretKeyLength(); + String secret = StringUtils.generateSafeToken(mobileSecretKeyLength == null ? DEFAULT_MOBILE_SECRET_KEY_LENGTH : mobileSecretKeyLength); cache.put(secret, tokenFactory.createTokenPair(securityUser)); return secret; } @@ -58,9 +58,9 @@ public class QRServiceImpl extends AbstractCachedService { +public class MobileSecretCaffeineCache extends CaffeineTbTransactionalCache { - public QRSecretCaffeineCache(CacheManager cacheManager) { - super(cacheManager, CacheConstants.QR_SECRET_KEY_CACHE); + public MobileSecretCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.MOBILE_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/MobileSecretEvictEvent.java similarity index 95% rename from application/src/main/java/org/thingsboard/server/service/qr/QRSecretEvictEvent.java rename to application/src/main/java/org/thingsboard/server/service/qr/MobileSecretEvictEvent.java index bf0c7b67fe..1f97e9ab07 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/QRSecretEvictEvent.java +++ b/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretEvictEvent.java @@ -18,7 +18,7 @@ package org.thingsboard.server.service.qr; import lombok.Data; @Data -public class QRSecretEvictEvent { +public class MobileSecretEvictEvent { 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/MobileSecretRedisCache.java similarity index 78% rename from application/src/main/java/org/thingsboard/server/service/qr/QRSecretRedisCache.java rename to application/src/main/java/org/thingsboard/server/service/qr/MobileSecretRedisCache.java index 8efe664187..d5cb25c909 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/QRSecretRedisCache.java +++ b/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretRedisCache.java @@ -28,9 +28,9 @@ import org.thingsboard.server.common.data.security.model.JwtPair; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("QRSecretCache") -public class QRSecretRedisCache extends RedisTbTransactionalCache { +public class MobileSecretRedisCache extends RedisTbTransactionalCache { - public QRSecretRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { - super(CacheConstants.QR_SECRET_KEY_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(JwtPair.class)); + public MobileSecretRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.MOBILE_SECRET_KEY_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(JwtPair.class)); } } 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 a804565f59..b12e6d4581 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 @@ -74,7 +74,7 @@ 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; + public static final int DEFAULT_MOBILE_SECRET_KEY_LENGTH = 64; @Autowired private AdminSettingsService adminSettingsService; @@ -110,7 +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); + securitySettings.setMobileSecretKeyLength(DEFAULT_MOBILE_SECRET_KEY_LENGTH); } return securitySettings; } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 2a0c8693ad..e010797d88 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -582,9 +582,9 @@ cache: 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 + 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 # 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 2d1ae9fdbb..430ff2eb09 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java @@ -23,7 +23,6 @@ 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; @@ -182,27 +181,4 @@ 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/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java new file mode 100644 index 0000000000..16624cefb1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java @@ -0,0 +1,132 @@ +/** + * 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.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Value; +import org.thingsboard.server.common.data.mobile.AndroidConfig; +import org.thingsboard.server.common.data.mobile.IosConfig; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.common.data.mobile.QRCodeConfig; +import org.thingsboard.server.common.data.security.model.JwtPair; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +@DaoSqlTest +public class MobileApplicationControllerTest extends AbstractControllerTest { + + @Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}") + private int mobileSecretKeyTtl; + private static final String ANDROID_PACKAGE_NAME = "testAppPackage"; + private static final String ANDROID_APP_SHA256 = "DF:28:32:66:8B:A7:D3:EC:7D:73:CF:CC"; + private static final String APPLE_APP_ID = "testId"; + private static final String TEST_LABEL = "Test label"; + + @Before + public void setUp() throws Exception { + loginSysAdmin(); + + QRCodeConfig qrCodeConfig = new QRCodeConfig(); + qrCodeConfig.setQrCodeLabel(TEST_LABEL); + + MobileAppSettings mobileAppSettings = new MobileAppSettings(); + mobileAppSettings.setUseDefault(true); + AndroidConfig androidConfig = AndroidConfig.builder() + .appPackage(ANDROID_PACKAGE_NAME) + .sha256CertFingerprints(ANDROID_APP_SHA256) + .enabled(true) + .build(); + + IosConfig iosConfig = IosConfig.builder() + .appId(APPLE_APP_ID) + .enabled(true) + .build(); + mobileAppSettings.setAndroidConfig(androidConfig); + mobileAppSettings.setIosConfig(iosConfig); + mobileAppSettings.setQrCodeConfig(qrCodeConfig); + + doPost("/api/mobile/app/settings", mobileAppSettings); + } + + @Test + public void testSaveMobileAppSettings() throws Exception { + loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + assertThat(mobileAppSettings.getQrCodeConfig().getQrCodeLabel()).isEqualTo(TEST_LABEL); + assertThat(mobileAppSettings.isUseDefault()).isTrue(); + + mobileAppSettings.setUseDefault(false); + + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isOk()); + + MobileAppSettings updatedMobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + assertThat(updatedMobileAppSettings.isUseDefault()).isFalse(); + } + + @Test + public void testGetApplicationAssociations() throws Exception { + JsonNode assetLinks = doGet("/.well-known/assetlinks.json", JsonNode.class); + assertThat(assetLinks.get(0).get("target").get("package_name").asText()).isEqualTo(ANDROID_PACKAGE_NAME); + assertThat(assetLinks.get(0).get("target").get("sha256_cert_fingerprints").get(0).asText()).isEqualTo(ANDROID_APP_SHA256); + + JsonNode appleAssociation = doGet("/.well-known/apple-app-site-association", JsonNode.class); + assertThat(appleAssociation.get("applinks").get("details").get(0).get("appID").asText()).isEqualTo(APPLE_APP_ID); + } + + @Test + public void testGetMobileDeepLink() throws Exception { + loginSysAdmin(); + String deepLink = doGet("/api/mobile/deepLink", String.class); + + Pattern expectedPattern = Pattern.compile("https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)&host=([^&]+)"); + Matcher parsedDeepLink = expectedPattern.matcher(deepLink); + assertThat(parsedDeepLink.matches()).isTrue(); + String secret = parsedDeepLink.group(2); + String ttl = parsedDeepLink.group(3); + assertThat(ttl).isEqualTo(String.valueOf(mobileSecretKeyTtl)); + + JwtPair jwtPair = doGet("/api/noauth/qr/" + secret, JwtPair.class); + assertThat(jwtPair).isNotNull(); + + loginTenantAdmin(); + String tenantDeepLink = doGet("/api/mobile/deepLink", String.class); + Matcher tenantParsedDeepLink = expectedPattern.matcher(tenantDeepLink); + assertThat(tenantParsedDeepLink.matches()).isTrue(); + String tenantSecret = tenantParsedDeepLink.group(2); + + JwtPair tenantJwtPair = doGet("/api/noauth/qr/" + tenantSecret, JwtPair.class); + assertThat(tenantJwtPair).isNotNull(); + + loginCustomerUser(); + String customerDeepLink = doGet("/api/mobile/deepLink", String.class); + Matcher customerParsedDeepLink = expectedPattern.matcher(customerDeepLink); + assertThat(customerParsedDeepLink.matches()).isTrue(); + String customerSecret = customerParsedDeepLink.group(2); + + JwtPair customerJwtPair = doGet("/api/noauth/qr/" + customerSecret, JwtPair.class); + assertThat(customerJwtPair).isNotNull(); + } +} diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 2e5944e0dc..92959e4552 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -8,7 +8,6 @@ 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 8eb8cd8678..b05dac9d4d 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 @@ -47,5 +47,5 @@ public class CacheConstants { 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"; + public static final String MOBILE_SECRET_KEY_CACHE = "mobileSecretKey"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java new file mode 100644 index 0000000000..e1fb5056fc --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java @@ -0,0 +1,38 @@ +/** + * 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 lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.validation.NoXss; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class AndroidConfig { + + private boolean enabled; + @NoXss + private String appPackage; + @NoXss + private String sha256CertFingerprints; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java new file mode 100644 index 0000000000..89c46a5c12 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java @@ -0,0 +1,23 @@ +/** + * 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; + +public enum BadgePosition { + + RIGHT, + LEFT; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgeStyle.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgeStyle.java new file mode 100644 index 0000000000..51606a2340 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgeStyle.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.common.data.mobile; + + +public enum BadgeStyle { + + ORIGINAL, + WHITE; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java new file mode 100644 index 0000000000..f03b446b3b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.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.common.data.mobile; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.validation.NoXss; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class IosConfig { + + private boolean enabled; + @NoXss + private String appId; + +} 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 index 039143f4a9..9153265950 100644 --- 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 @@ -15,49 +15,24 @@ */ package org.thingsboard.server.common.data.mobile; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.databind.JsonNode; +import jakarta.validation.Valid; 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; + boolean useDefault; + @Valid + private AndroidConfig androidConfig; + @Valid + private IosConfig iosConfig; + @Valid + private QRCodeConfig qrCodeConfig; - @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/mobile/QRCodeConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java new file mode 100644 index 0000000000..20de410137 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java @@ -0,0 +1,38 @@ +/** + * 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 lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.validation.NoXss; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class QRCodeConfig { + + private boolean showOnHomePage; + private boolean badgeEnabled; + private boolean labelEnabled; + private BadgePosition badgePosition; + private BadgeStyle badgeStyle; + @NoXss + private String qrCodeLabel; + +} 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 b6ee1a17a5..aa4d303440 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,6 +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; + @Schema(description = "Mobile secret key length" ) + private Integer mobileSecretKeyLength; } 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 index fa035cc32d..1c05cfbb92 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java @@ -15,35 +15,27 @@ */ 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.BadgePosition; +import org.thingsboard.server.common.data.mobile.BadgeStyle; import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.common.data.mobile.QRCodeConfig; 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 static final String DEFAULT_QR_CODE_LABEL = "Scan to connect or download mobile app"; 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; @@ -66,27 +58,18 @@ public class BaseMobileAppSettingsService extends AbstractCachedService, Seria @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_USE_DEFAULT_PROPERTY) + private boolean useDefault; - @Column(name = ModelConstants.MOBILE_APP_SETTINGS_SHA256_CERT_FINGERPRINTS_PROPERTY) - private String sha256CertFingerprints; + @Convert(converter = JsonConverter.class) + @Column(name = ModelConstants.MOBILE_APP_SETTINGS_ANDROID_CONFIG_PROPERTY) + private JsonNode androidConfig; - @Column(name = ModelConstants.MOBILE_APP_APP_ID_PROPERTY) - private String appId; + @Convert(converter = JsonConverter.class) + @Column(name = ModelConstants.MOBILE_APP_IOS_CONFIG_PROPERTY) + private JsonNode iosConfig; @Convert(converter = JsonConverter.class) - @Column(name = ModelConstants.MOBILE_APP_SETTINGS_PROPERTY) - private JsonNode settings; + @Column(name = ModelConstants.MOBILE_APP_QR_CODE_CONFIG_PROPERTY) + private JsonNode qrCodeConfig; public MobileAppSettingsEntity(MobileAppSettings mobileAppSettings) { this.tenantId = mobileAppSettings.getTenantId().getId(); - if (mobileAppSettings.getAppPackage() != null) { - this.appPackage = mobileAppSettings.getAppPackage(); + this.useDefault = mobileAppSettings.isUseDefault(); + if (mobileAppSettings.getAndroidConfig() != null) { + this.androidConfig = JacksonUtil.valueToTree(mobileAppSettings.getAndroidConfig()); } - if (mobileAppSettings.getSha256CertFingerprints() != null) { - this.sha256CertFingerprints = mobileAppSettings.getSha256CertFingerprints(); + if (mobileAppSettings.getIosConfig() != null) { + this.iosConfig = JacksonUtil.valueToTree(mobileAppSettings.getIosConfig()); } - if (mobileAppSettings.getAppId() != null) { - this.appId = mobileAppSettings.getAppId(); - } - if (mobileAppSettings.getSettings() != null) { - this.settings = mobileAppSettings.getSettings(); + if (mobileAppSettings.getQrCodeConfig() != null) { + this.qrCodeConfig = JacksonUtil.valueToTree(mobileAppSettings.getQrCodeConfig()); } } @@ -75,17 +79,15 @@ public class MobileAppSettingsEntity implements ToData, Seria public MobileAppSettings toData() { MobileAppSettings mobileAppSettings = new MobileAppSettings(); mobileAppSettings.setTenantId(TenantId.fromUUID(tenantId)); - if (appPackage != null) { - mobileAppSettings.setAppPackage(appPackage); - } - if (sha256CertFingerprints != null) { - mobileAppSettings.setSha256CertFingerprints(sha256CertFingerprints); + mobileAppSettings.setUseDefault(useDefault); + if (qrCodeConfig != null) { + mobileAppSettings.setAndroidConfig(JacksonUtil.convertValue(androidConfig, AndroidConfig.class)); } - if (appId != null) { - mobileAppSettings.setAppId(appId); + if (qrCodeConfig != null) { + mobileAppSettings.setIosConfig(JacksonUtil.convertValue(iosConfig, IosConfig.class)); } - if (settings != null) { - mobileAppSettings.setSettings(settings); + if (qrCodeConfig != null) { + mobileAppSettings.setQrCodeConfig(JacksonUtil.convertValue(qrCodeConfig, QRCodeConfig.class)); } 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 index f445cb0829..8c732e0ffc 100644 --- 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 @@ -36,8 +36,8 @@ public class JpaMobileAppDao extends JpaAbstractDaoListeningExecutorService impl private MobileAppSettingsRepository mobileAppSettingsRepository; @Override - public MobileAppSettings save(TenantId tenantId, MobileAppSettings whiteLabeling) { - return DaoUtil.getData(mobileAppSettingsRepository.save(new MobileAppSettingsEntity(whiteLabeling))); + public MobileAppSettings save(TenantId tenantId, MobileAppSettings mobileAppSettings) { + return DaoUtil.getData(mobileAppSettingsRepository.save(new MobileAppSettingsEntity(mobileAppSettings))); } @Override diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index cc6b950be9..a984302f04 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -897,8 +897,8 @@ CREATE TABLE IF NOT EXISTS queue_stats ( 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) + use_default boolean, + android_config VARCHAR(1000), + ios_config VARCHAR(1000), + qr_code_config VARCHAR(100000) ); \ No newline at end of file From b216898924bfdae9704742afb038feb82ecc0e33 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 24 Apr 2024 20:07:29 +0300 Subject: [PATCH 03/13] updated default mobile app settings, fixed permission --- .../MobileApplicationController.java | 2 +- .../service/qr/MobileSecretCaffeineCache.java | 2 +- .../service/qr/MobileSecretRedisCache.java | 2 +- .../permission/CustomerUserPermissions.java | 1 + .../permission/TenantAdminPermissions.java | 2 +- .../MobileApplicationControllerTest.java | 28 ++++++++++++++++++ .../common/data/mobile/QRCodeConfig.java | 4 ++- .../mobile/BaseMobileAppSettingsService.java | 29 ++++++++++++++----- 8 files changed, 57 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java index a65d8506ff..0cd0884126 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java @@ -135,7 +135,7 @@ public class MobileApplicationController extends BaseController { SecurityUser currentUser = getCurrentUser(); accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ); - return mobileAppSettingsService.getMobileAppSettings(currentUser.getTenantId()); + return mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); } @ApiOperation(value = "Get the deep link to the associated mobile application (getMobileAppDeepLink)", diff --git a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretCaffeineCache.java b/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretCaffeineCache.java index fb4c5e64cc..916d58afd7 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretCaffeineCache.java +++ b/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretCaffeineCache.java @@ -23,7 +23,7 @@ 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") +@Service("MobileSecretCache") public class MobileSecretCaffeineCache extends CaffeineTbTransactionalCache { public MobileSecretCaffeineCache(CacheManager cacheManager) { diff --git a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretRedisCache.java b/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretRedisCache.java index d5cb25c909..d29a104ae9 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretRedisCache.java +++ b/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretRedisCache.java @@ -27,7 +27,7 @@ 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") +@Service("MobileSecretCache") public class MobileSecretRedisCache extends RedisTbTransactionalCache { public MobileSecretRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { 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 d26e49e4ab..af1b34ae71 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 @@ -49,6 +49,7 @@ public class CustomerUserPermissions extends AbstractPermissions { put(Resource.DEVICE_PROFILE, profilePermissionChecker); put(Resource.ASSET_PROFILE, profilePermissionChecker); put(Resource.TB_RESOURCE, customerResourcePermissionChecker); + put(Resource.MOBILE_APP_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/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index f4117e1a36..10807e4b5a 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,7 +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); + put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ)); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java index 16624cefb1..9e493980b1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java @@ -104,8 +104,10 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { Pattern expectedPattern = Pattern.compile("https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)&host=([^&]+)"); Matcher parsedDeepLink = expectedPattern.matcher(deepLink); assertThat(parsedDeepLink.matches()).isTrue(); + String appHost = parsedDeepLink.group(1); String secret = parsedDeepLink.group(2); String ttl = parsedDeepLink.group(3); + assertThat(appHost).isEqualTo("demo.thingsboard.io"); assertThat(ttl).isEqualTo(String.valueOf(mobileSecretKeyTtl)); JwtPair jwtPair = doGet("/api/noauth/qr/" + secret, JwtPair.class); @@ -128,5 +130,31 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { JwtPair customerJwtPair = doGet("/api/noauth/qr/" + customerSecret, JwtPair.class); assertThat(customerJwtPair).isNotNull(); + + // update mobile setting to use custom one + loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + mobileAppSettings.setUseDefault(false); + doPost("/api/mobile/app/settings", mobileAppSettings); + + String customAppDeepLink = doGet("/api/mobile/deepLink", String.class); + Pattern customAppExpectedPattern = Pattern.compile("https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)"); + Matcher customAppParsedDeepLink = customAppExpectedPattern.matcher(customAppDeepLink); + assertThat(customAppParsedDeepLink.matches()).isTrue(); + assertThat(customAppParsedDeepLink.group(1)).isEqualTo("localhost"); + + loginTenantAdmin(); + String tenantCustomAppDeepLink = doGet("/api/mobile/deepLink", String.class); + Matcher tenantCustomAppParsedDeepLink = customAppExpectedPattern.matcher(tenantCustomAppDeepLink); + assertThat(tenantCustomAppParsedDeepLink.matches()).isTrue(); + assertThat(tenantCustomAppParsedDeepLink.group(1)).isEqualTo("localhost"); + + loginCustomerUser(); + String customerCustomAppDeepLink = doGet("/api/mobile/deepLink", String.class); + Matcher customerCustomAppParsedDeepLink = customAppExpectedPattern.matcher(customerCustomAppDeepLink); + assertThat(customerCustomAppParsedDeepLink.matches()).isTrue(); + assertThat(customerCustomAppParsedDeepLink.group(1)).isEqualTo("localhost"); + + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java index 20de410137..b3f975870e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java @@ -16,12 +16,14 @@ package org.thingsboard.server.common.data.mobile; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.validation.NoXss; @Data +@Builder @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode @@ -29,7 +31,7 @@ public class QRCodeConfig { private boolean showOnHomePage; private boolean badgeEnabled; - private boolean labelEnabled; + private boolean qrCodeLabelEnabled; private BadgePosition badgePosition; private BadgeStyle badgeStyle; @NoXss 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 index 1c05cfbb92..d11edafb1b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java @@ -20,8 +20,10 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.AndroidConfig; import org.thingsboard.server.common.data.mobile.BadgePosition; import org.thingsboard.server.common.data.mobile.BadgeStyle; +import org.thingsboard.server.common.data.mobile.IosConfig; import org.thingsboard.server.common.data.mobile.MobileAppSettings; import org.thingsboard.server.common.data.mobile.QRCodeConfig; import org.thingsboard.server.dao.entity.AbstractCachedService; @@ -45,7 +47,7 @@ public class BaseMobileAppSettingsService extends AbstractCachedService mobileAppSettingsDao.findByTenantId(tenantId), false); + () -> mobileAppSettingsDao.findByTenantId(tenantId), true); return constructMobileAppSettings(mobileAppSettings); } @@ -60,14 +62,25 @@ public class BaseMobileAppSettingsService extends AbstractCachedService Date: Thu, 25 Apr 2024 18:24:27 +0300 Subject: [PATCH 04/13] added mobile app settings cleanup for tenant deletion --- .../src/main/data/upgrade/3.6.4/schema_update.sql | 6 +++--- .../controller/MobileApplicationController.java | 13 +++++-------- .../controller/MobileApplicationControllerTest.java | 10 +++++----- .../common/data/mobile/MobileAppSettings.java | 2 +- .../dao/mobile/BaseMobileAppSettingsService.java | 12 +++++++++++- .../server/dao/mobile/MobileAppSettingsService.java | 2 ++ .../server/dao/model/ModelConstants.java | 2 +- .../dao/model/sql/MobileAppSettingsEntity.java | 8 ++++---- .../server/dao/tenant/TenantServiceImpl.java | 6 ++++++ dao/src/main/resources/sql/schema-entities.sql | 2 +- 10 files changed, 39 insertions(+), 24 deletions(-) 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 b7bbdc761f..5f00b7c2cf 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 @@ -135,14 +135,14 @@ DELETE FROM asset_profile WHERE name ='TbServiceQueue'; -- QUEUE STATS UPDATE END --- MOBILE APPS TABLE CREATE START +-- MOBILE APP SETTINGS TABLE CREATE START CREATE TABLE IF NOT EXISTS mobile_app_settings ( tenant_id UUID NOT NULL, - use_default boolean, + use_default_app boolean, android_config VARCHAR(1000), ios_config VARCHAR(1000), qr_code_config VARCHAR(100000) ); --- MOBILE APPS TABLE CREATE END \ No newline at end of file +-- MOBILE APP SETTINGS TABLE CREATE END \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java index 0cd0884126..e8ac13b077 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java @@ -106,7 +106,6 @@ public class MobileApplicationController extends BaseController { public ResponseEntity getAppleAppSiteAssociation() { MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); IosConfig iosConfig = mobileAppSettings.getIosConfig(); - if (iosConfig != null && iosConfig.isEnabled() && !iosConfig.getAppId().isBlank()) { return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, iosConfig.getAppId()))); } else { @@ -115,7 +114,7 @@ public class MobileApplicationController extends BaseController { } @ApiOperation(value = "Create Or Update the Mobile application settings (saveMobileAppSettings)", - notes = "The payload contains associated android and iOS applications and platform qr code widget settings." + SYSTEM_AUTHORITY_PARAGRAPH) + notes = "The request payload contains configuration for android/iOS applications and platform qr code widget settings." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PostMapping(value = "/api/mobile/app/settings") public MobileAppSettings saveMobileAppSettings(@Parameter(description = "A JSON value representing the mobile apps configuration") @@ -128,7 +127,7 @@ public class MobileApplicationController extends BaseController { } @ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)", - notes = "The payload contains associated android and iOS applications and platform qr code widget settings." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + notes = "The response payload contains configuration for android/iOS applications and platform qr code widget settings." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/api/mobile/app/settings") public MobileAppSettings getMobileAppSettings() throws ThingsboardException { @@ -139,18 +138,16 @@ public class MobileApplicationController extends BaseController { } @ApiOperation(value = "Get the deep link to the associated mobile application (getMobileAppDeepLink)", - notes = "Fetch the url that takes user to associated mobile application " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + notes = "Fetch the url that takes user to linked mobile application " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/api/mobile/deepLink", produces = "text/plain") public String getMobileAppDeepLink(HttpServletRequest request) throws ThingsboardException, URISyntaxException { - SecurityUser currentUser = getCurrentUser(); - String secret = mobileAppSecretService.generateMobileAppSecret(currentUser); - + String secret = mobileAppSecretService.generateMobileAppSecret(getCurrentUser()); String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request); String platformDomain = new URI(baseUrl).getHost(); MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); String appDomain; - if (!mobileAppSettings.isUseDefault()) { + if (!mobileAppSettings.isUseDefaultApp()) { appDomain = platformDomain; } else { appDomain = DEFAULT_APP_DOMAIN; diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java index 9e493980b1..b7dd9b5b3b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java @@ -52,7 +52,7 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { qrCodeConfig.setQrCodeLabel(TEST_LABEL); MobileAppSettings mobileAppSettings = new MobileAppSettings(); - mobileAppSettings.setUseDefault(true); + mobileAppSettings.setUseDefaultApp(true); AndroidConfig androidConfig = AndroidConfig.builder() .appPackage(ANDROID_PACKAGE_NAME) .sha256CertFingerprints(ANDROID_APP_SHA256) @@ -75,15 +75,15 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { loginSysAdmin(); MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); assertThat(mobileAppSettings.getQrCodeConfig().getQrCodeLabel()).isEqualTo(TEST_LABEL); - assertThat(mobileAppSettings.isUseDefault()).isTrue(); + assertThat(mobileAppSettings.isUseDefaultApp()).isTrue(); - mobileAppSettings.setUseDefault(false); + mobileAppSettings.setUseDefaultApp(false); doPost("/api/mobile/app/settings", mobileAppSettings) .andExpect(status().isOk()); MobileAppSettings updatedMobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); - assertThat(updatedMobileAppSettings.isUseDefault()).isFalse(); + assertThat(updatedMobileAppSettings.isUseDefaultApp()).isFalse(); } @Test @@ -134,7 +134,7 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { // update mobile setting to use custom one loginSysAdmin(); MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); - mobileAppSettings.setUseDefault(false); + mobileAppSettings.setUseDefaultApp(false); doPost("/api/mobile/app/settings", mobileAppSettings); String customAppDeepLink = doGet("/api/mobile/deepLink", String.class); 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 index 9153265950..217db862f8 100644 --- 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 @@ -27,7 +27,7 @@ public class MobileAppSettings implements Serializable { private static final long serialVersionUID = 2628323657987010348L; private TenantId tenantId; - boolean useDefault; + private boolean useDefaultApp; @Valid private AndroidConfig androidConfig; @Valid 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 index d11edafb1b..a72498b3b2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java @@ -28,11 +28,14 @@ import org.thingsboard.server.common.data.mobile.MobileAppSettings; import org.thingsboard.server.common.data.mobile.QRCodeConfig; import org.thingsboard.server.dao.entity.AbstractCachedService; +import static org.thingsboard.server.dao.service.Validator.validateId; + @Service @Slf4j @RequiredArgsConstructor public class BaseMobileAppSettingsService extends AbstractCachedService implements MobileAppSettingsService { + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; private static final String DEFAULT_QR_CODE_LABEL = "Scan to connect or download mobile app"; private final MobileAppSettingsDao mobileAppSettingsDao; @@ -51,6 +54,13 @@ public class BaseMobileAppSettingsService extends AbstractCachedService INCORRECT_TENANT_ID + id); + mobileAppSettingsDao.removeByTenantId(tenantId); + } + @TransactionalEventListener(classes = MobileAppSettingsEvictEvent.class) @Override public void handleEvictEvent(MobileAppSettingsEvictEvent event) { @@ -60,7 +70,7 @@ public class BaseMobileAppSettingsService extends AbstractCachedService, Seria @Column(name = ModelConstants.TENANT_ID_COLUMN, columnDefinition = "uuid") protected UUID tenantId; - @Column(name = ModelConstants.MOBILE_APP_SETTINGS_USE_DEFAULT_PROPERTY) - private boolean useDefault; + @Column(name = ModelConstants.MOBILE_APP_SETTINGS_USE_DEFAULT_APP_PROPERTY) + private boolean useDefaultApp; @Convert(converter = JsonConverter.class) @Column(name = ModelConstants.MOBILE_APP_SETTINGS_ANDROID_CONFIG_PROPERTY) @@ -63,7 +63,7 @@ public class MobileAppSettingsEntity implements ToData, Seria public MobileAppSettingsEntity(MobileAppSettings mobileAppSettings) { this.tenantId = mobileAppSettings.getTenantId().getId(); - this.useDefault = mobileAppSettings.isUseDefault(); + this.useDefaultApp = mobileAppSettings.isUseDefaultApp(); if (mobileAppSettings.getAndroidConfig() != null) { this.androidConfig = JacksonUtil.valueToTree(mobileAppSettings.getAndroidConfig()); } @@ -79,7 +79,7 @@ public class MobileAppSettingsEntity implements ToData, Seria public MobileAppSettings toData() { MobileAppSettings mobileAppSettings = new MobileAppSettings(); mobileAppSettings.setTenantId(TenantId.fromUUID(tenantId)); - mobileAppSettings.setUseDefault(useDefault); + mobileAppSettings.setUseDefaultApp(useDefaultApp); if (qrCodeConfig != null) { mobileAppSettings.setAndroidConfig(JacksonUtil.convertValue(androidConfig, AndroidConfig.class)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 8b3e65c927..7d256ceb27 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -42,6 +42,7 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import org.thingsboard.server.dao.mobile.MobileAppSettingsService; import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationSettingsService; @@ -137,6 +138,10 @@ public class TenantServiceImpl extends AbstractCachedEntityService Date: Tue, 30 Apr 2024 18:46:38 +0300 Subject: [PATCH 05/13] added id, createdTime to mobile app setting entity, added MobileAppSettingsValidator --- .../main/data/upgrade/3.6.4/schema_update.sql | 4 +- .../MobileApplicationController.java | 27 +++++- .../MobileApplicationControllerTest.java | 85 +++++++++++++++++-- .../common/data/id/MobileAppSettingsId.java | 37 ++++++++ .../common/data/mobile/MobileAppSettings.java | 13 ++- .../mobile/BaseMobileAppSettingsService.java | 30 +++++-- .../dao/mobile/MobileAppSettingsDao.java | 5 +- .../server/dao/model/ModelConstants.java | 4 +- .../model/sql/MobileAppSettingsEntity.java | 44 ++++------ .../MobileAppSettingsDataValidator.java | 51 +++++++++++ ...pDao.java => JpaMobileAppSettingsDao.java} | 22 +++-- .../mobile/MobileAppSettingsRepository.java | 2 + .../main/resources/sql/schema-entities.sql | 4 +- 13 files changed, 268 insertions(+), 60 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java rename dao/src/main/java/org/thingsboard/server/dao/sql/mobile/{JpaMobileAppDao.java => JpaMobileAppSettingsDao.java} (73%) 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 5f00b7c2cf..03e8ca0a29 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 @@ -138,7 +138,9 @@ DELETE FROM asset_profile WHERE name ='TbServiceQueue'; -- MOBILE APP SETTINGS TABLE CREATE START CREATE TABLE IF NOT EXISTS mobile_app_settings ( - tenant_id UUID NOT NULL, + id uuid NOT NULL CONSTRAINT mobile_app_settings_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid UNIQUE NOT NULL, use_default_app boolean, android_config VARCHAR(1000), ios_config VARCHAR(1000), diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java index e8ac13b077..96b1bf4d35 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java @@ -20,17 +20,17 @@ import io.swagger.v3.oas.annotations.Parameter; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; 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.AndroidConfig; import org.thingsboard.server.common.data.mobile.IosConfig; @@ -81,10 +81,13 @@ public class MobileApplicationController extends BaseController { " }\n" + "}"; + public static final String ANDROID_APPLICATION_STORE_LINK = "https://play.google.com/store/apps/details?id=org.thingsboard.demo.app"; + public static final String APPLE_APPLICATION_STORE_LINK = "https://apps.apple.com/us/app/thingsboard-live/id1594355695"; 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&ttl=%s"; + private final SystemSecurityService systemSecurityService; private final MobileAppSecretService mobileAppSecretService; private final MobileAppSettingsService mobileAppSettingsService; @@ -143,7 +146,7 @@ public class MobileApplicationController extends BaseController { @GetMapping(value = "/api/mobile/deepLink", produces = "text/plain") public String getMobileAppDeepLink(HttpServletRequest request) throws ThingsboardException, URISyntaxException { String secret = mobileAppSecretService.generateMobileAppSecret(getCurrentUser()); - String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request); + String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, null, request); String platformDomain = new URI(baseUrl).getHost(); MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); String appDomain; @@ -156,7 +159,7 @@ public class MobileApplicationController extends BaseController { if (!appDomain.equals(platformDomain)) { deepLink = deepLink + "&host=" + baseUrl; } - return deepLink; + return "\"" + deepLink + "\""; } @ApiOperation(value = "Get User Token (getUserTokenByMobileSecret)", @@ -167,4 +170,20 @@ public class MobileApplicationController extends BaseController { checkParameter(SECRET, secret); return mobileAppSecretService.getJwtPair(secret); } + + @GetMapping(value = "/api/noauth/qr") + public ResponseEntity getApplicationRedirect(@RequestHeader(value = "User-Agent") String userAgent) { + if (userAgent.contains("Android")) { + return ResponseEntity.status(HttpStatus.FOUND) + .header("Location", ANDROID_APPLICATION_STORE_LINK) + .build(); + } else if (userAgent.contains("iPhone") || userAgent.contains("iPad")) { + return ResponseEntity.status(HttpStatus.FOUND) + .header("Location", APPLE_APPLICATION_STORE_LINK) + .build(); + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .build(); + } + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java index b7dd9b5b3b..c4f1301457 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java @@ -31,6 +31,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Slf4j @@ -48,10 +49,10 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { public void setUp() throws Exception { loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); QRCodeConfig qrCodeConfig = new QRCodeConfig(); qrCodeConfig.setQrCodeLabel(TEST_LABEL); - MobileAppSettings mobileAppSettings = new MobileAppSettings(); mobileAppSettings.setUseDefaultApp(true); AndroidConfig androidConfig = AndroidConfig.builder() .appPackage(ANDROID_PACKAGE_NAME) @@ -67,7 +68,8 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { mobileAppSettings.setIosConfig(iosConfig); mobileAppSettings.setQrCodeConfig(qrCodeConfig); - doPost("/api/mobile/app/settings", mobileAppSettings); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isOk()); } @Test @@ -86,6 +88,79 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { assertThat(updatedMobileAppSettings.isUseDefaultApp()).isFalse(); } + @Test + public void testShouldNotSaveMobileAppSettingsWithoutRequiredConfig() throws Exception { + loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + + mobileAppSettings.setUseDefaultApp(false); + mobileAppSettings.setAndroidConfig(null); + mobileAppSettings.setIosConfig(null); + mobileAppSettings.setQrCodeConfig(null); + + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Android/ios settings are required to use custom application!"))); + + mobileAppSettings.setAndroidConfig(AndroidConfig.builder().enabled(false).build()); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Android/ios settings are required to use custom application!"))); + + mobileAppSettings.setIosConfig(IosConfig.builder().enabled(false).build()); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Qr code config is required!"))); + + mobileAppSettings.setQrCodeConfig(QRCodeConfig.builder().showOnHomePage(false).build()); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isOk()); + } + + @Test + public void testShouldNotSaveMobileAppSettingsWithoutRequiredAndroidConf() throws Exception { + loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + AndroidConfig androidConfig = AndroidConfig.builder() + .enabled(true) + .appPackage(null) + .sha256CertFingerprints(null) + .build(); + mobileAppSettings.setAndroidConfig(androidConfig); + + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for enabled android settings!"))); + + androidConfig.setAppPackage("test_app_package"); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for enabled android settings!"))); + + androidConfig.setSha256CertFingerprints("test_sha_256"); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isOk()); + } + + @Test + public void testShouldNotSaveMobileAppSettingsWithoutRequiredIosConf() throws Exception { + loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + IosConfig iosConfig = IosConfig.builder() + .enabled(true) + .appId(null) + .build(); + mobileAppSettings.setIosConfig(iosConfig); + + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Application id is required for enabled ios settings!"))); + + iosConfig.setAppId("test_app_id"); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isOk()); + } + @Test public void testGetApplicationAssociations() throws Exception { JsonNode assetLinks = doGet("/.well-known/assetlinks.json", JsonNode.class); @@ -101,7 +176,7 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { loginSysAdmin(); String deepLink = doGet("/api/mobile/deepLink", String.class); - Pattern expectedPattern = Pattern.compile("https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)&host=([^&]+)"); + Pattern expectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)&host=([^&]+)\""); Matcher parsedDeepLink = expectedPattern.matcher(deepLink); assertThat(parsedDeepLink.matches()).isTrue(); String appHost = parsedDeepLink.group(1); @@ -138,7 +213,7 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { doPost("/api/mobile/app/settings", mobileAppSettings); String customAppDeepLink = doGet("/api/mobile/deepLink", String.class); - Pattern customAppExpectedPattern = Pattern.compile("https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)"); + Pattern customAppExpectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)\""); Matcher customAppParsedDeepLink = customAppExpectedPattern.matcher(customAppDeepLink); assertThat(customAppParsedDeepLink.matches()).isTrue(); assertThat(customAppParsedDeepLink.group(1)).isEqualTo("localhost"); @@ -154,7 +229,5 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { Matcher customerCustomAppParsedDeepLink = customAppExpectedPattern.matcher(customerCustomAppDeepLink); assertThat(customerCustomAppParsedDeepLink.matches()).isTrue(); assertThat(customerCustomAppParsedDeepLink.group(1)).isEqualTo("localhost"); - - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java new file mode 100644 index 0000000000..d152701fc4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java @@ -0,0 +1,37 @@ +/** + * 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.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.UUID; + +@Schema +public class MobileAppSettingsId extends UUIDBased { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public MobileAppSettingsId(@JsonProperty("id") UUID id) { + super(id); + } + + public static MobileAppSettingsId fromString(String mobileAppSettingsId) { + return new MobileAppSettingsId(UUID.fromString(mobileAppSettingsId)); + } +} 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 index 217db862f8..9a7b88f22c 100644 --- 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 @@ -17,12 +17,13 @@ package org.thingsboard.server.common.data.mobile; import jakarta.validation.Valid; import lombok.Data; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.MobileAppSettingsId; import org.thingsboard.server.common.data.id.TenantId; -import java.io.Serializable; - @Data -public class MobileAppSettings implements Serializable { +public class MobileAppSettings extends BaseData implements HasTenantId { private static final long serialVersionUID = 2628323657987010348L; @@ -35,4 +36,10 @@ public class MobileAppSettings implements Serializable { @Valid private QRCodeConfig qrCodeConfig; + public MobileAppSettings() { + } + public MobileAppSettings(MobileAppSettingsId id) { + super(id); + } + } 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 index a72498b3b2..86de46fed1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.mobile; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.id.TenantId; @@ -26,24 +27,41 @@ import org.thingsboard.server.common.data.mobile.BadgeStyle; import org.thingsboard.server.common.data.mobile.IosConfig; import org.thingsboard.server.common.data.mobile.MobileAppSettings; import org.thingsboard.server.common.data.mobile.QRCodeConfig; -import org.thingsboard.server.dao.entity.AbstractCachedService; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; import static org.thingsboard.server.dao.service.Validator.validateId; @Service @Slf4j @RequiredArgsConstructor -public class BaseMobileAppSettingsService extends AbstractCachedService implements MobileAppSettingsService { +public class BaseMobileAppSettingsService extends AbstractCachedEntityService implements MobileAppSettingsService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; private static final String DEFAULT_QR_CODE_LABEL = "Scan to connect or download mobile app"; + private final MobileAppSettingsDao mobileAppSettingsDao; + private final DataValidator mobileAppSettingsDataValidator; @Override - public MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings settings) { - MobileAppSettings mobileAppSettings = mobileAppSettingsDao.save(tenantId, settings); - publishEvictEvent(new MobileAppSettingsEvictEvent(tenantId)); - return mobileAppSettings; + public MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings mobileAppSettings) { + mobileAppSettingsDataValidator.validate(mobileAppSettings, s -> tenantId); + try { + MobileAppSettings savedMobileAppSettings = mobileAppSettingsDao.save(tenantId, mobileAppSettings); + publishEvictEvent(new MobileAppSettingsEvictEvent(tenantId)); + return savedMobileAppSettings; + } catch (Exception exception) { + if (mobileAppSettings != null) { + handleEvictEvent(new MobileAppSettingsEvictEvent(tenantId)); + } + ConstraintViolationException e = extractConstraintViolationException(exception).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("mobile_app_settings_tenant_id_key")) { + throw new DataValidationException("Mobile application for specified tenant already exists!"); + } else { + throw exception; + } + } } @Override 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 index 54661bccb0..094ead1a5b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java @@ -17,11 +17,10 @@ package org.thingsboard.server.dao.mobile; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.dao.Dao; -public interface MobileAppSettingsDao { - - MobileAppSettings save(TenantId tenantId, MobileAppSettings appSettings); +public interface MobileAppSettingsDao extends Dao { MobileAppSettings findByTenantId(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 2b770cbc8e..623d899293 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 @@ -665,8 +665,8 @@ public class ModelConstants { public static final String MOBILE_APP_SETTINGS_TABLE_NAME = "mobile_app_settings"; public static final String MOBILE_APP_SETTINGS_USE_DEFAULT_APP_PROPERTY = "use_default_app"; public static final String MOBILE_APP_SETTINGS_ANDROID_CONFIG_PROPERTY = "android_config"; - public static final String MOBILE_APP_IOS_CONFIG_PROPERTY = "ios_config"; - public static final String MOBILE_APP_QR_CODE_CONFIG_PROPERTY = "qr_code_config"; + public static final String MOBILE_APP_SETTINGS_IOS_CONFIG_PROPERTY = "ios_config"; + public static final String MOBILE_APP_SETTINGS_QR_CODE_CONFIG_PROPERTY = "qr_code_config"; 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}; 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 index aa3f77a58d..0e6ad933dc 100644 --- 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 @@ -19,30 +19,27 @@ 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.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.MobileAppSettingsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mobile.AndroidConfig; import org.thingsboard.server.common.data.mobile.IosConfig; import org.thingsboard.server.common.data.mobile.MobileAppSettings; import org.thingsboard.server.common.data.mobile.QRCodeConfig; +import org.thingsboard.server.dao.model.BaseSqlEntity; 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 { +public class MobileAppSettingsEntity extends BaseSqlEntity { - @Id @Column(name = ModelConstants.TENANT_ID_COLUMN, columnDefinition = "uuid") protected UUID tenantId; @@ -54,41 +51,34 @@ public class MobileAppSettingsEntity implements ToData, Seria private JsonNode androidConfig; @Convert(converter = JsonConverter.class) - @Column(name = ModelConstants.MOBILE_APP_IOS_CONFIG_PROPERTY) + @Column(name = ModelConstants.MOBILE_APP_SETTINGS_IOS_CONFIG_PROPERTY) private JsonNode iosConfig; @Convert(converter = JsonConverter.class) - @Column(name = ModelConstants.MOBILE_APP_QR_CODE_CONFIG_PROPERTY) + @Column(name = ModelConstants.MOBILE_APP_SETTINGS_QR_CODE_CONFIG_PROPERTY) private JsonNode qrCodeConfig; public MobileAppSettingsEntity(MobileAppSettings mobileAppSettings) { + if (mobileAppSettings.getId() != null) { + this.setId(mobileAppSettings.getId().getId()); + } + this.setCreatedTime(mobileAppSettings.getCreatedTime()); this.tenantId = mobileAppSettings.getTenantId().getId(); this.useDefaultApp = mobileAppSettings.isUseDefaultApp(); - if (mobileAppSettings.getAndroidConfig() != null) { - this.androidConfig = JacksonUtil.valueToTree(mobileAppSettings.getAndroidConfig()); - } - if (mobileAppSettings.getIosConfig() != null) { - this.iosConfig = JacksonUtil.valueToTree(mobileAppSettings.getIosConfig()); - } - if (mobileAppSettings.getQrCodeConfig() != null) { - this.qrCodeConfig = JacksonUtil.valueToTree(mobileAppSettings.getQrCodeConfig()); - } + this.androidConfig = toJson(mobileAppSettings.getAndroidConfig()); + this.iosConfig = toJson(mobileAppSettings.getIosConfig()); + this.qrCodeConfig = toJson(mobileAppSettings.getQrCodeConfig()); } @Override public MobileAppSettings toData() { - MobileAppSettings mobileAppSettings = new MobileAppSettings(); + MobileAppSettings mobileAppSettings = new MobileAppSettings(new MobileAppSettingsId(getUuid())); + mobileAppSettings.setCreatedTime(createdTime); mobileAppSettings.setTenantId(TenantId.fromUUID(tenantId)); mobileAppSettings.setUseDefaultApp(useDefaultApp); - if (qrCodeConfig != null) { - mobileAppSettings.setAndroidConfig(JacksonUtil.convertValue(androidConfig, AndroidConfig.class)); - } - if (qrCodeConfig != null) { - mobileAppSettings.setIosConfig(JacksonUtil.convertValue(iosConfig, IosConfig.class)); - } - if (qrCodeConfig != null) { - mobileAppSettings.setQrCodeConfig(JacksonUtil.convertValue(qrCodeConfig, QRCodeConfig.class)); - } + mobileAppSettings.setAndroidConfig(fromJson(androidConfig, AndroidConfig.class)); + mobileAppSettings.setIosConfig(fromJson(iosConfig, IosConfig.class)); + mobileAppSettings.setQrCodeConfig(fromJson(qrCodeConfig, QRCodeConfig.class)); return mobileAppSettings; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java new file mode 100644 index 0000000000..6400cd08af --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java @@ -0,0 +1,51 @@ +/** + * 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.service.validator; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.AndroidConfig; +import org.thingsboard.server.common.data.mobile.IosConfig; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.common.data.mobile.QRCodeConfig; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; + +@Component +@AllArgsConstructor +public class MobileAppSettingsDataValidator extends DataValidator { + + @Override + protected void validateDataImpl(TenantId tenantId, MobileAppSettings mobileAppSettings) { + AndroidConfig androidConfig = mobileAppSettings.getAndroidConfig(); + IosConfig iosConfig = mobileAppSettings.getIosConfig(); + QRCodeConfig qrCodeConfig = mobileAppSettings.getQrCodeConfig(); + if (!mobileAppSettings.isUseDefaultApp() && (androidConfig == null || iosConfig == null)) { + throw new DataValidationException("Android/ios settings are required to use custom application!"); + } + if (qrCodeConfig == null) { + throw new DataValidationException("Qr code config is required!"); + } + if (androidConfig != null && androidConfig.isEnabled() && + (androidConfig.getAppPackage() == null || androidConfig.getSha256CertFingerprints() == null)) { + throw new DataValidationException("Application package and sha256 cert fingerprints are required for enabled android settings!"); + } + if (iosConfig != null && iosConfig.isEnabled() && iosConfig.getAppId() == null) { + throw new DataValidationException("Application id is required for enabled ios settings!"); + } + } +} 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/JpaMobileAppSettingsDao.java similarity index 73% rename from dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppSettingsDao.java index 8c732e0ffc..e4286bb949 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppSettingsDao.java @@ -17,32 +17,31 @@ package org.thingsboard.server.dao.sql.mobile; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.JpaRepository; 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.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; +import java.util.UUID; + @Component @Slf4j @SqlDao -public class JpaMobileAppDao extends JpaAbstractDaoListeningExecutorService implements MobileAppSettingsDao { +public class JpaMobileAppSettingsDao extends JpaAbstractDao implements MobileAppSettingsDao { @Autowired private MobileAppSettingsRepository mobileAppSettingsRepository; - @Override - public MobileAppSettings save(TenantId tenantId, MobileAppSettings mobileAppSettings) { - return DaoUtil.getData(mobileAppSettingsRepository.save(new MobileAppSettingsEntity(mobileAppSettings))); - } @Override public MobileAppSettings findByTenantId(TenantId tenantId) { - return DaoUtil.getData(mobileAppSettingsRepository.findById(tenantId.getId())); + return DaoUtil.getData(mobileAppSettingsRepository.findByTenantId(tenantId.getId())); } @Override @@ -50,4 +49,13 @@ public class JpaMobileAppDao extends JpaAbstractDaoListeningExecutorService impl mobileAppSettingsRepository.deleteByTenantId(tenantId.getId()); } + @Override + protected Class getEntityClass() { + return MobileAppSettingsEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return mobileAppSettingsRepository; + } } 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 index a2339e1354..bb6e62f3fe 100644 --- 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 @@ -27,6 +27,8 @@ import java.util.UUID; public interface MobileAppSettingsRepository extends JpaRepository { + MobileAppSettingsEntity findByTenantId(@Param("tenantId") UUID tenantId); + @Transactional @Modifying @Query("DELETE FROM MobileAppSettingsEntity r WHERE r.tenantId = :tenantId") diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 72dae5394b..91df2e81bc 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -896,7 +896,9 @@ CREATE TABLE IF NOT EXISTS queue_stats ( ); CREATE TABLE IF NOT EXISTS mobile_app_settings ( - tenant_id UUID NOT NULL, + id uuid NOT NULL CONSTRAINT mobile_app_settings_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid UNIQUE NOT NULL, use_default_app boolean, android_config VARCHAR(1000), ios_config VARCHAR(1000), From f9348633886edd5bed543a6bcb8463031375f373 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 2 May 2024 12:56:47 +0300 Subject: [PATCH 06/13] fixed api test --- .../server/config/ThingsboardSecurityConfiguration.java | 2 +- .../server/controller/MobileApplicationController.java | 2 -- .../server/controller/MobileApplicationControllerTest.java | 6 ++++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index eda8d3e89d..c83f6500b2 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -77,7 +77,7 @@ public class ThingsboardSecurityConfiguration { public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; - protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**"}; + protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**", "/.well-known/**"}; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String WS_ENTRY_POINT = "/api/ws/**"; public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code"; diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java index 96b1bf4d35..ddd49ab975 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java @@ -125,7 +125,6 @@ public class MobileApplicationController extends BaseController { SecurityUser currentUser = getCurrentUser(); accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.WRITE); mobileAppSettings.setTenantId(getTenantId()); - return mobileAppSettingsService.saveMobileAppSettings(currentUser.getTenantId(), mobileAppSettings); } @@ -136,7 +135,6 @@ public class MobileApplicationController extends BaseController { public MobileAppSettings getMobileAppSettings() throws ThingsboardException { SecurityUser currentUser = getCurrentUser(); accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ); - return mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); } diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java index c4f1301457..ad049295ea 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java @@ -163,6 +163,12 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { @Test public void testGetApplicationAssociations() throws Exception { + loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + mobileAppSettings.setUseDefaultApp(false); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isOk()); + JsonNode assetLinks = doGet("/.well-known/assetlinks.json", JsonNode.class); assertThat(assetLinks.get(0).get("target").get("package_name").asText()).isEqualTo(ANDROID_PACKAGE_NAME); assertThat(assetLinks.get(0).get("target").get("sha256_cert_fingerprints").get(0).asText()).isEqualTo(ANDROID_APP_SHA256); From 0bf5290a329ed5ac9b43a0b7dc1cd66e2d5e8128 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 2 May 2024 19:33:39 +0300 Subject: [PATCH 07/13] code formatting --- .../server/config/ThingsboardSecurityConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 693dd4bfec..a3111cb137 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -76,7 +76,7 @@ public class ThingsboardSecurityConfiguration { public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; - protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**", "/.well-known/**"}; + protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[]{"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**", "/.well-known/**"}; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String WS_ENTRY_POINT = "/api/ws/**"; public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code"; From a057f42e95425b629d86dfd5687dd14932703321 Mon Sep 17 00:00:00 2001 From: rusikv Date: Thu, 2 May 2024 21:37:11 +0300 Subject: [PATCH 08/13] UI: mobile app QR code UI initial implementation --- .../widget_bundles/home_page_widgets.json | 3 +- .../widget_types/mobile_app_qr_code.json | 21 ++ .../src/app/core/http/mobile-app.service.ts | 45 ++++ ui-ngx/src/app/core/services/menu.service.ts | 8 + ui-ngx/src/app/core/services/utils.service.ts | 5 +- .../mobile-app-qrcode-widget.component.html | 40 ++++ .../mobile-app-qrcode-widget.component.scss | 56 +++++ .../lib/mobile-app-qrcode-widget.component.ts | 111 ++++++++++ .../widget/widget-components.module.ts | 3 + .../home/pages/admin/admin-routing.module.ts | 14 ++ .../modules/home/pages/admin/admin.module.ts | 18 +- .../admin/mobile-app-settings.component.html | 161 ++++++++++++++ .../admin/mobile-app-settings.component.scss | 28 +++ .../admin/mobile-app-settings.component.ts | 197 ++++++++++++++++++ .../app/shared/models/mobile-app.models.ts | 81 +++++++ .../android_store_en_black_badge.svg | 22 ++ .../ios_store_en_black_badge.svg | 46 ++++ .../ios_store_en_white_badge.svg | 46 ++++ .../dashboard/tenant_admin_home_page.json | 53 ++++- .../assets/locale/locale.constant-en_US.json | 27 +++ ui-ngx/src/form.scss | 15 ++ 21 files changed, 987 insertions(+), 13 deletions(-) create mode 100644 application/src/main/data/json/system/widget_types/mobile_app_qr_code.json create mode 100644 ui-ngx/src/app/core/http/mobile-app.service.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts create mode 100644 ui-ngx/src/app/shared/models/mobile-app.models.ts create mode 100644 ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg create mode 100755 ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_black_badge.svg create mode 100755 ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_white_badge.svg diff --git a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json index a30d6ee76f..5465f750e5 100644 --- a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json @@ -13,6 +13,7 @@ "home_page_widgets.quick_links", "home_page_widgets.documentation_links", "home_page_widgets.dashboards", - "home_page_widgets.usage_info" + "home_page_widgets.usage_info", + "home_page_widgets.mobile_app_qr_code" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json new file mode 100644 index 0000000000..2e3600d05b --- /dev/null +++ b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json @@ -0,0 +1,21 @@ +{ + "fqn": "home_page_widgets.mobile_app_qr_code", + "name": "Mobile app QR code", + "deprecated": false, + "image": null, + "description": null, + "descriptor": { + "type": "static", + "sizeX": 6, + "sizeY": 3, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mobileAppQrcodeWidget.ngOnInit();\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.mobileAppQrcodeWidget.ngOnDestroy();\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "", + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"HTML Card\",\"dropShadow\":true}" + }, + "tags": null +} \ No newline at end of file diff --git a/ui-ngx/src/app/core/http/mobile-app.service.ts b/ui-ngx/src/app/core/http/mobile-app.service.ts new file mode 100644 index 0000000000..75d5f337bc --- /dev/null +++ b/ui-ngx/src/app/core/http/mobile-app.service.ts @@ -0,0 +1,45 @@ +/// +/// 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. +/// + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { MobileAppSettings } from '@shared/models/mobile-app.models'; + +@Injectable({ + providedIn: 'root' +}) +export class MobileAppService { + + constructor( + private http: HttpClient + ) { + } + + public getMobileAppSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); + } + + public saveMobileAppSettings(mobileAppSettings: MobileAppSettings, config?: RequestConfig): Observable { + return this.http.post(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); + } + + public getMobileAppDeepLink( config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/deepLink`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 1f2d76a8b8..ffb5b5f0a6 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -245,6 +245,14 @@ export class MenuService { path: '/settings/queues', icon: 'swap_calls' }, + { + id: 'mobile_app_settings', + name: 'admin.mobile-app.mobile-app', + fullName: 'admin.mobile-app.mobile-app', + type: 'link', + path: '/settings/mobile-app', + icon: 'smartphone' + } ] }, { diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 70b04ee4eb..d7238c450d 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -30,7 +30,7 @@ import { guid, hashCode, isDefined, - isDefinedAndNotNull, + isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined, objToBase64, @@ -386,8 +386,7 @@ export class UtilsService { this.window.performance.now() : Date.now(); } - public getQueryParam(name: string): string { - const url = this.window.location.href; + public getQueryParam(name: string, url = this.window.location.href): string { name = name.replace(/[\[\]]/g, '\\$&'); const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); const results = regex.exec(url); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html new file mode 100644 index 0000000000..53f95cb57e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html @@ -0,0 +1,40 @@ + +
admin.mobile-app.connect-mobile-app
+
+
+ +
+ + + + + + + + +
+
+
+ {{ mobileAppSettings.qrCodeConfig.qrCodeLabel }} +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss new file mode 100644 index 0000000000..1d088ec482 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss @@ -0,0 +1,56 @@ +/** + * 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. + */ + +@import '../../../../../../scss/constants'; + +:host { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + background-color: transparent; +} + +.tb-title { + align-self: start; + font-weight: 600; + font-size: 20px; + line-height: 24px; + letter-spacing: 0.1px; + color: rgba(0, 0, 0, 0.76); + @media #{$mat-md-lg} { + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + } +} + +.tb-qrcode-label { + font-size: 14px; + color: rgba(0, 0, 0, 0.54); +} + +.tb-qrcode { + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.08); + border-radius: 6px; +} + +.tb-badge-container { + @media #{$mat-lt-lg} { + display: none; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts new file mode 100644 index 0000000000..c9c8541613 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts @@ -0,0 +1,111 @@ +/// +/// 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. +/// + +import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { AppState } from '@core/core.state'; +import { Store } from '@ngrx/store'; +import { BadgePosition, BadgeStyle, badgeStyleURLMap, MobileAppSettings } from '@shared/models/mobile-app.models'; +import { MobileAppService } from '@core/http/mobile-app.service'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { UtilsService } from '@core/services/utils.service'; +import { interval, mergeMap, Observable, Subject, takeUntil } from 'rxjs'; +import { MINUTE } from '@shared/models/time/time.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-mobile-app-qrcode-widget', + templateUrl: './mobile-app-qrcode-widget.component.html', + styleUrls: ['./mobile-app-qrcode-widget.component.scss'] +}) +export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { + + @Input() + ctx: WidgetContext; + + @Input() + @coerceBoolean() + previewMode: boolean; + + @Input() + set mobileAppSettings(settings: MobileAppSettings) { + if (settings) { + this.mobileAppSettingsValue = settings; + } + }; + + get mobileAppSettings() { + return this.mobileAppSettingsValue; + } + + @ViewChild('canvas', {static: false}) canvasRef: ElementRef; + + private readonly destroy$ = new Subject(); + + badgeStyle = BadgeStyle; + badgePosition = BadgePosition; + badgeStyleURLMap = badgeStyleURLMap; + + private mobileAppSettingsValue: MobileAppSettings; + private deepLinkTTL: number; + + constructor(protected store: Store, + protected cd: ChangeDetectorRef, + private mobileAppService: MobileAppService, + private utilsService: UtilsService) { + super(store); + } + + ngOnInit(): void { + if (this.ctx) { + this.ctx.$scope.mobileAppQrcodeWidget = this; + } + if (!this.mobileAppSettings) { + this.mobileAppService.getMobileAppSettings().subscribe((settings => { + this.mobileAppSettings = settings; + this.cd.detectChanges(); + })); + } + } + + ngAfterViewInit(): void { + this.getMobileAppDeepLink().subscribe(link => { + this.deepLinkTTL = Number(this.utilsService.getQueryParam('ttl', link)) * MINUTE; + this.updateQRCode(link); + interval(this.deepLinkTTL).pipe( + takeUntil(this.destroy$), + mergeMap(() => this.getMobileAppDeepLink()) + ).subscribe(link => this.updateQRCode(link)); + }); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.destroy$.next(); + this.destroy$.complete(); + } + + getMobileAppDeepLink(): Observable { + return this.mobileAppService.getMobileAppDeepLink(); + } + + updateQRCode(link: string) { + import('qrcode').then((QRCode) => { + QRCode.toCanvas(this.canvasRef.nativeElement, link, { width: 125}); + }); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 69185b6dd2..e25f0d3925 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -92,6 +92,7 @@ import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-c import { BarChartWidgetComponent } from '@home/components/widget/lib/chart/bar-chart-widget.component'; import { PolarAreaWidgetComponent } from '@home/components/widget/lib/chart/polar-area-widget.component'; import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/radar-chart-widget.component'; +import { MobileAppQrcodeWidgetComponent } from '@home/components/widget/lib/mobile-app-qrcode-widget.component'; @NgModule({ declarations: @@ -112,6 +113,7 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad NavigationCardsWidgetComponent, NavigationCardWidgetComponent, QrCodeWidgetComponent, + MobileAppQrcodeWidgetComponent, MarkdownWidgetComponent, SelectEntityDialogComponent, LegendComponent, @@ -176,6 +178,7 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad NavigationCardsWidgetComponent, NavigationCardWidgetComponent, QrCodeWidgetComponent, + MobileAppQrcodeWidgetComponent, MarkdownWidgetComponent, LegendComponent, FlotWidgetComponent, diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index 63fb9a14de..f05fc05a25 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -40,6 +40,7 @@ import { widgetsLibraryRoutes } from '@home/pages/widget/widget-library-routing. import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module'; import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component'; +import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component'; @Injectable() export class OAuth2LoginProcessingUrlResolver implements Resolve { @@ -270,6 +271,19 @@ const routes: Routes = [ } } }, + { + path: 'mobile-app', + component: MobileAppSettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN], + title: 'admin.mobile-app.mobile-app', + breadcrumb: { + label: 'admin.mobile-app.mobile-app', + icon: 'smartphone' + } + } + }, { path: 'security-settings', redirectTo: '/security-settings/general' diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index 1ab60687a3..f44782e814 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -33,6 +33,8 @@ import { QueueComponent } from '@home/pages/admin/queue/queue.component'; import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component'; import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component'; import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component'; +import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component'; +import { WidgetComponentsModule } from '@home/components/widget/widget-components.module'; @NgModule({ declarations: @@ -49,13 +51,15 @@ import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-aut QueueComponent, RepositoryAdminSettingsComponent, AutoCommitAdminSettingsComponent, - TwoFactorAuthSettingsComponent + TwoFactorAuthSettingsComponent, + MobileAppSettingsComponent ], - imports: [ - CommonModule, - SharedModule, - HomeComponentsModule, - AdminRoutingModule - ] + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + AdminRoutingModule, + WidgetComponentsModule + ] }) export class AdminModule { } diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html new file mode 100644 index 0000000000..5b1cb710e0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html @@ -0,0 +1,161 @@ + + + + + admin.mobile-app.mobile-app-qr-code-widget-settings + + + + +
+ +
+
+
+
admin.mobile-app.applications
+ + {{ 'admin.mobile-app.default' | translate }} + {{ 'admin.mobile-app.custom' | translate }} + +
+
+
+ + {{ 'admin.mobile-app.android' | translate }} + +
+
+
+
{{ 'admin.mobile-app.app-package-name' | translate }}
+ + + + warning + + +
+
+
+
+
{{ 'admin.mobile-app.sha256-certificate-fingerprints' | translate }}
+ + + + warning + + +
+
+
+
+
+ + {{ 'admin.mobile-app.ios' | translate }} + +
+
+
+
{{ 'admin.mobile-app.app-id' | translate }}
+ + + + warning + + +
+
+
+
+
+
+
admin.mobile-app.appearance-on-home-page
+ + {{ 'admin.mobile-app.enabled' | translate }} + {{ 'admin.mobile-app.disabled' | translate }} + +
+
+
+ + {{ 'admin.mobile-app.badges' | translate }} + + + + + {{ badgeStyle.value | translate }} + + + + + + + {{ badgePosition.value | translate }} + + + +
+
+
+
+ + {{ 'admin.mobile-app.label' | translate }} + + + + +
+
+
+
admin.mobile-app.preview
+ + +
+
+
+
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss new file mode 100644 index 0000000000..5f04c04e8f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss @@ -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. + */ + +.tb-fixed-width { + min-width: 230px; +} + +.tb-qrcode-preview { + background-color: rgba(0, 0, 0, 0.04); + box-shadow: none; + + .tb-form-panel-title { + color: rgba(0, 0, 0, 0.38); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts new file mode 100644 index 0000000000..c342b68d1f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts @@ -0,0 +1,197 @@ +/// +/// 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. +/// + +import { Component, OnDestroy } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; +import { Subject, takeUntil } from 'rxjs'; +import { MobileAppService } from '@core/http/mobile-app.service'; +import { + BadgePosition, + badgePositionTranslationsMap, + BadgeStyle, + badgeStyleTranslationsMap, + MobileAppSettings +} from '@shared/models/mobile-app.models'; + +@Component({ + selector: 'tb-mobile-app-settings', + templateUrl: './mobile-app-settings.component.html', + styleUrls: ['mobile-app-settings.component.scss', './settings-card.scss'] +}) +export class MobileAppSettingsComponent extends PageComponent implements HasConfirmForm, OnDestroy { + + mobileAppSettingsForm: FormGroup; + + mobileAppSettings: MobileAppSettings; + + private readonly destroy$ = new Subject(); + + badgePositionTranslationsMap = badgePositionTranslationsMap; + badgeStyleTranslationsMap = badgeStyleTranslationsMap; + + constructor(protected store: Store, + private mobileAppService: MobileAppService, + public fb: FormBuilder) { + super(store); + this.buildMobileAppSettingsForm(); + this.mobileAppService.getMobileAppSettings() + .subscribe(settings => this.processMobileAppSettings(settings)); + this.mobileAppSettingsForm.get('useDefaultApp').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (value) { + this.mobileAppSettingsForm.get('androidConfig.appPackage').clearValidators(); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators(); + this.mobileAppSettingsForm.get('iosConfig.appId').clearValidators(); + } else { + this.mobileAppSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]); + this.mobileAppSettingsForm.get('iosConfig.appId').setValidators([Validators.required]); + } + this.mobileAppSettingsForm.get('androidConfig.appPackage').updateValueAndValidity(); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').updateValueAndValidity(); + this.mobileAppSettingsForm.get('iosConfig.appId').updateValueAndValidity(); + }); + this.mobileAppSettingsForm.get('androidConfig.enabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { + if (value) { + this.mobileAppSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]); + } else { + this.mobileAppSettingsForm.get('androidConfig.appPackage').clearValidators(); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators(); + } + this.mobileAppSettingsForm.get('androidConfig.appPackage').updateValueAndValidity(); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').updateValueAndValidity(); + } + if (this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { + if (value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable(); + if (this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable(); + } + } else { + if (!this.mobileAppSettingsForm.get('iosConfig.enabled').value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable(); + } + } + } + }); + this.mobileAppSettingsForm.get('iosConfig.enabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { + if (value) { + this.mobileAppSettingsForm.get('iosConfig.appId').setValidators([Validators.required]); + } else { + this.mobileAppSettingsForm.get('iosConfig.appId').clearValidators(); + } + this.mobileAppSettingsForm.get('iosConfig.appId').updateValueAndValidity(); + } + if (this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { + if (value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable(); + if (this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable(); + } + } else { + if (!this.mobileAppSettingsForm.get('androidConfig.enabled').value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable(); + } + } + } + }); + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (value) { + if (this.mobileAppSettingsForm.get('androidConfig.enabled').value || this.mobileAppSettingsForm.get('iosConfig.enabled').value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable(); + } + } else { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable(); + } + }); + this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (value) { + this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').enable(); + } else { + this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').disable(); + } + }); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.destroy$.next(); + this.destroy$.complete(); + } + + private buildMobileAppSettingsForm() { + this.mobileAppSettingsForm = this.fb.group({ + useDefaultApp: [true, []], + androidConfig: this.fb.group({ + enabled: [true, []], + appPackage: ['', []], + sha256CertFingerprints: ['', []] + }), + iosConfig: this.fb.group({ + enabled: [true, []], + appId: ['', []] + }), + qrCodeConfig: this.fb.group({ + showOnHomePage: [true, []], + badgeEnabled: [true, []], + badgeStyle: [{value: BadgeStyle.ORIGINAL, disabled: true}, []], + badgePosition: [{value: BadgePosition.RIGHT, disabled: true}, []], + qrCodeLabelEnabled: [true, []], + qrCodeLabel: ['', []] + }) + }); + } + + private processMobileAppSettings(mobileAppSettings: MobileAppSettings): void { + this.mobileAppSettings = {...mobileAppSettings}; + this.mobileAppSettingsForm.reset(this.mobileAppSettings); + } + + save(): void { + this.mobileAppSettings = {...this.mobileAppSettings, ...this.mobileAppSettingsForm.getRawValue()}; + this.mobileAppService.saveMobileAppSettings(this.mobileAppSettings) + .subscribe((settings) => this.processMobileAppSettings(settings)); + } + + confirmForm(): FormGroup { + return this.mobileAppSettingsForm; + } + +} diff --git a/ui-ngx/src/app/shared/models/mobile-app.models.ts b/ui-ngx/src/app/shared/models/mobile-app.models.ts new file mode 100644 index 0000000000..0787f17747 --- /dev/null +++ b/ui-ngx/src/app/shared/models/mobile-app.models.ts @@ -0,0 +1,81 @@ +/// +/// 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. +/// + +import { TenantId } from '@shared/models/id/tenant-id'; + +export interface MobileAppSettings { + tenantId: TenantId; + useDefaultApp: boolean; + androidConfig: AndroidConfig; + iosConfig: IosConfig; + qrCodeConfig: QRCodeConfig; +} + +export interface AndroidConfig { + enabled: boolean; + appPackage: string; + sha256CertFingerprints: string +} + +export interface IosConfig { + enabled: boolean; + appId: string; +} + +export interface QRCodeConfig { + showOnHomePage: boolean; + badgeEnabled: boolean; + badgePosition: BadgePosition; + badgeStyle: BadgeStyle; + qrCodeLabelEnabled: boolean; + qrCodeLabel: string; +} + +export interface MobileOSBadgeURL { + iOS: string; + android: string; +} + +export enum BadgePosition { + RIGHT = 'RIGHT', + LEFT = 'LEFT' +} + +export const badgePositionTranslationsMap = new Map([ + [BadgePosition.RIGHT, 'admin.mobile-app.right'], + [BadgePosition.LEFT, 'admin.mobile-app.left'] +]); + +export enum BadgeStyle { + ORIGINAL = 'ORIGINAL', + WHITE = 'WHITE' +} + +export const badgeStyleTranslationsMap = new Map([ + [BadgeStyle.ORIGINAL, 'admin.mobile-app.original'], + [BadgeStyle.WHITE, 'admin.mobile-app.white'] +]); + +export const badgeStyleURLMap = new Map([ + [BadgeStyle.ORIGINAL, { + iOS: 'assets/android-ios-stores-badges/ios_store_en_black_badge.svg', + android: 'assets/android-ios-stores-badges/android_store_en_black_badge.svg' + }], + [BadgeStyle.WHITE, { + iOS: 'assets/android-ios-stores-badges/ios_store_en_white_badge.svg', + android: 'assets/android-ios-stores-badges/android_store_en_black_badge.svg' + }] +]); diff --git a/ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg b/ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg new file mode 100644 index 0000000000..02b88dab6f --- /dev/null +++ b/ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_black_badge.svg b/ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_black_badge.svg new file mode 100755 index 0000000000..072b425a1a --- /dev/null +++ b/ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_black_badge.svg @@ -0,0 +1,46 @@ + + Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_white_badge.svg b/ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_white_badge.svg new file mode 100755 index 0000000000..16c0496ca7 --- /dev/null +++ b/ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_white_badge.svg @@ -0,0 +1,46 @@ + + Download_on_the_App_Store_Badge_US-UK_RGB_wht_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json index 85669b8bf4..d850e640b7 100644 --- a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json @@ -1143,6 +1143,49 @@ "row": 0, "col": 0, "id": "8e71a398-caf5-540d-cec5-6e5dc264343e" + }, + "b9d704a2-f579-dbff-f123-f953d92081fd": { + "typeFullFqn": "system.home_page_widgets.mobile_app_qr_code", + "type": "static", + "sizeX": 6, + "sizeY": 3, + "config": { + "datasources": [ + { + "type": "static", + "name": "function", + "dataKeys": [ + { + "name": "f(x)", + "type": "function", + "label": "Random", + "color": "#2196f3", + "settings": {}, + "_hash": 0.15479322438769105, + "funcBody": "var value = prevValue + Math.random() * 100 - 50;\nvar multiplier = Math.pow(10, 2 || 0);\nvar value = Math.round(value * multiplier) / multiplier;\nif (value < -1000) {\n\tvalue = -1000;\n} else if (value > 1000) {\n\tvalue = 1000;\n}\nreturn value;" + } + ] + } + ], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": false, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "8px", + "settings": { + "cardHtml": "
HTML code here
", + "cardCss": ".card {\n font-weight: bold;\n font-size: 32px;\n color: #999;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n}" + }, + "title": "HTML Card", + "dropShadow": false + }, + "row": 0, + "col": 0, + "id": "b9d704a2-f579-dbff-f123-f953d92081fd" } }, "states": { @@ -1169,7 +1212,7 @@ }, "a23185ad-dc46-806c-0e50-5b21fb080ace": { "sizeX": 35, - "sizeY": 58, + "sizeY": 42, "row": 0, "col": 85, "mobileHide": true @@ -1212,6 +1255,12 @@ "row": 0, "col": 43, "mobileOrder": 0 + }, + "b9d704a2-f579-dbff-f123-f953d92081fd": { + "sizeX": 35, + "sizeY": 16, + "row": 42, + "col": 85 } }, "gridSettings": { @@ -1416,4 +1465,4 @@ } }, "name": "Tenant Administrator Home Page" -} \ No newline at end of file +} 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 b13d169cdc..1a50f9f62b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -419,6 +419,33 @@ "no-auto-commit-entities-prompt": "No entities configured for auto-commit", "delete-auto-commit-settings-title": "Are you sure you want to delete auto-commit settings?", "delete-auto-commit-settings-text": "Be careful, after the confirmation the auto-commit settings will be removed and auto-commit will be disabled for all entities.", + "mobile-app": { + "mobile-app": "Mobile app", + "mobile-app-qr-code-widget-settings": "Mobile app QR code widget settings", + "applications": "Applications", + "default": "Default", + "custom": "Custom", + "android": "Android", + "app-package-name": "App package name", + "app-package-name-required": "App package name is required", + "sha256-certificate-fingerprints": "SHA256 certificate fingerprints", + "sha256-certificate-fingerprints-required": "SHA256 certificate fingerprints is required", + "ios": "iOS", + "app-id": "App ID", + "app-id-required": "App ID is required", + "appearance-on-home-page": "Appearance on Home page", + "enabled": "Enabled", + "disabled": "Disabled", + "badges": "Badges", + "original": "Original", + "white": "White", + "label": "Label", + "right": "Right", + "left": "Left", + "set": "Set", + "preview": "Preview", + "connect-mobile-app": "Connect mobile app" + }, "2fa": { "2fa": "Two-factor authentication", "available-providers": "Available providers", diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index 1f9ca93c88..a692994144 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -257,9 +257,15 @@ &.row { flex-direction: row; } + &.row-reverse { + flex-direction: row-reverse; + } &.column { flex-direction: column; } + &.center { + justify-content: center; + } &.flex-start { justify-content: flex-start; } @@ -278,6 +284,15 @@ &.fill-width { width: 100%; } + &.fill-height { + height: 100%; + } + &.shrink { + flex: 0; + } + &.no-gap { + gap: 0; + } } .tb-form-panel, .tb-form-row { From 4ee969fa1ef5a6321e072f66d058909b2cc166ec Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Sun, 5 May 2024 18:12:26 +0300 Subject: [PATCH 09/13] fixed mobile application validator --- .../MobileApplicationControllerTest.java | 22 +++++++++++++++---- .../MobileAppSettingsDataValidator.java | 10 ++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java index ad049295ea..39e2ef6a40 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java @@ -110,7 +110,7 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { mobileAppSettings.setIosConfig(IosConfig.builder().enabled(false).build()); doPost("/api/mobile/app/settings", mobileAppSettings) .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Qr code config is required!"))); + .andExpect(statusReason(containsString("Qr code configuration is required!"))); mobileAppSettings.setQrCodeConfig(QRCodeConfig.builder().showOnHomePage(false).build()); doPost("/api/mobile/app/settings", mobileAppSettings) @@ -121,6 +121,7 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { public void testShouldNotSaveMobileAppSettingsWithoutRequiredAndroidConf() throws Exception { loginSysAdmin(); MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + mobileAppSettings.setUseDefaultApp(false); AndroidConfig androidConfig = AndroidConfig.builder() .enabled(true) .appPackage(null) @@ -130,12 +131,12 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { doPost("/api/mobile/app/settings", mobileAppSettings) .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for enabled android settings!"))); + .andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for custom android application!"))); androidConfig.setAppPackage("test_app_package"); doPost("/api/mobile/app/settings", mobileAppSettings) .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for enabled android settings!"))); + .andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for custom android application!"))); androidConfig.setSha256CertFingerprints("test_sha_256"); doPost("/api/mobile/app/settings", mobileAppSettings) @@ -146,6 +147,7 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { public void testShouldNotSaveMobileAppSettingsWithoutRequiredIosConf() throws Exception { loginSysAdmin(); MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + mobileAppSettings.setUseDefaultApp(false); IosConfig iosConfig = IosConfig.builder() .enabled(true) .appId(null) @@ -154,13 +156,25 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { doPost("/api/mobile/app/settings", mobileAppSettings) .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Application id is required for enabled ios settings!"))); + .andExpect(statusReason(containsString("Application id is required for custom ios application!"))); iosConfig.setAppId("test_app_id"); doPost("/api/mobile/app/settings", mobileAppSettings) .andExpect(status().isOk()); } + @Test + public void testShouldSaveMobileAppSettingsForDefaultApp() throws Exception { + loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + mobileAppSettings.setUseDefaultApp(true); + mobileAppSettings.setIosConfig(null); + mobileAppSettings.setAndroidConfig(null); + + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isOk()); + } + @Test public void testGetApplicationAssociations() throws Exception { loginSysAdmin(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java index 6400cd08af..c867b08cc4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java @@ -38,14 +38,14 @@ public class MobileAppSettingsDataValidator extends DataValidator Date: Tue, 7 May 2024 11:08:39 +0300 Subject: [PATCH 10/13] UI: added separate mobile qrcode widget to cards bundle --- .../json/system/widget_bundles/cards.json | 1 + .../widget_bundles/home_page_widgets.json | 2 +- .../widget_types/home_mobile_app_qr_code.json | 21 +++ .../widget_types/mobile_app_qr_code.json | 10 +- .../src/app/core/http/mobile-app.service.ts | 10 +- .../cards/mobile-app-qr-code-widge.models.ts | 50 ++++++ .../mobile-app-qrcode-widget.component.html | 6 +- .../mobile-app-qrcode-widget.component.scss | 3 +- .../lib/mobile-app-qrcode-widget.component.ts | 14 +- ...app-qr-code-widget-settings.component.html | 130 +++++++++++++++ ...app-qr-code-widget-settings.component.scss | 19 +++ ...e-app-qr-code-widget-settings.component.ts | 156 ++++++++++++++++++ .../lib/settings/widget-settings.module.ts | 6 + .../admin/mobile-app-settings.component.ts | 6 +- .../app/shared/models/mobile-app.models.ts | 2 +- .../dashboard/tenant_admin_home_page.json | 12 +- ui-ngx/src/form.scss | 3 - 17 files changed, 415 insertions(+), 36 deletions(-) create mode 100644 application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widge.models.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 39996839f1..5a839d4c82 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -17,6 +17,7 @@ "cards.label_widget", "cards.dashboard_state_widget", "cards.qr_code", + "cards.mobile_app_qr_code", "cards.attributes_card", "cards.html_card", "cards.html_value_card", diff --git a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json index 5465f750e5..0af30e4df2 100644 --- a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json @@ -14,6 +14,6 @@ "home_page_widgets.documentation_links", "home_page_widgets.dashboards", "home_page_widgets.usage_info", - "home_page_widgets.mobile_app_qr_code" + "home_page_widgets.home_mobile_app_qr_code" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json new file mode 100644 index 0000000000..88109fc902 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json @@ -0,0 +1,21 @@ +{ + "fqn": "home_page_widgets.home_mobile_app_qr_code", + "name": "Mobile app QR code", + "deprecated": false, + "image": null, + "description": null, + "descriptor": { + "type": "static", + "sizeX": 6, + "sizeY": 3, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "", + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"Mobile app QR code\",\"dropShadow\":true}" + }, + "tags": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json index 2e3600d05b..82ce9025ed 100644 --- a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json +++ b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json @@ -1,21 +1,21 @@ { - "fqn": "home_page_widgets.mobile_app_qr_code", + "fqn": "cards.mobile_app_qr_code", "name": "Mobile app QR code", "deprecated": false, "image": null, "description": null, "descriptor": { "type": "static", - "sizeX": 6, + "sizeX": 7.5, "sizeY": 3, "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mobileAppQrcodeWidget.ngOnInit();\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.mobileAppQrcodeWidget.ngOnDestroy();\n}", + "controllerScript": "self.onInit = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", - "settingsDirective": "", - "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"HTML Card\",\"dropShadow\":true}" + "settingsDirective": "tb-mobile-app-qr-code-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"useDefaultApp\":true,\"androidConfig\":{\"enabled\":true,\"appPackage\":\"\",\"sha256CertFingerprints\":\"\"},\"iosConfig\":{\"enabled\":true,\"appId\":\"\"},\"qrCodeConfig\":{\"badgeEnabled\":true,\"badgeStyle\":\"ORIGINAL\",\"badgePosition\":\"RIGHT\",\"qrCodeLabelEnabled\":true,\"qrCodeLabel\":\"Scan to connect or download mobile app\"}},\"title\":\"Mobile app QR code\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" }, "tags": null } \ No newline at end of file diff --git a/ui-ngx/src/app/core/http/mobile-app.service.ts b/ui-ngx/src/app/core/http/mobile-app.service.ts index 75d5f337bc..bb4dc6c863 100644 --- a/ui-ngx/src/app/core/http/mobile-app.service.ts +++ b/ui-ngx/src/app/core/http/mobile-app.service.ts @@ -18,7 +18,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { Observable } from 'rxjs'; -import { MobileAppSettings } from '@shared/models/mobile-app.models'; +import { MobileAppQRCodeSettings } from '@shared/models/mobile-app.models'; @Injectable({ providedIn: 'root' @@ -30,12 +30,12 @@ export class MobileAppService { ) { } - public getMobileAppSettings(config?: RequestConfig): Observable { - return this.http.get(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); + public getMobileAppSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); } - public saveMobileAppSettings(mobileAppSettings: MobileAppSettings, config?: RequestConfig): Observable { - return this.http.post(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); + public saveMobileAppSettings(mobileAppSettings: MobileAppQRCodeSettings, config?: RequestConfig): Observable { + return this.http.post(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); } public getMobileAppDeepLink( config?: RequestConfig): Observable { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widge.models.ts new file mode 100644 index 0000000000..4027e0445b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widge.models.ts @@ -0,0 +1,50 @@ +/// +/// 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. +/// + +import { + AndroidConfig, + BadgePosition, + BadgeStyle, + IosConfig, + QRCodeConfig +} from '@shared/models/mobile-app.models'; + +export type MobileAppQrCodeWidgetSettings = { + useDefaultApp: boolean; + androidConfig: AndroidConfig; + iosConfig: IosConfig; + qrCodeConfig: Omit; +} + +export const mobileAppQrCodeWidgetDefaultSettings: MobileAppQrCodeWidgetSettings = { + useDefaultApp: true, + androidConfig: { + enabled: true, + appPackage: '', + sha256CertFingerprints: '' + }, + iosConfig: { + enabled: true, + appId: '' + }, + qrCodeConfig: { + badgeEnabled: true, + badgeStyle: BadgeStyle.ORIGINAL, + badgePosition: BadgePosition.RIGHT, + qrCodeLabelEnabled: true, + qrCodeLabel: 'Scan to connect or download mobile app' + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html index 53f95cb57e..e3f9ca11c3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html @@ -15,15 +15,15 @@ limitations under the License. --> -
admin.mobile-app.connect-mobile-app
+
admin.mobile-app.connect-mobile-app
-
+ [class.tb-badge-container]="!previewMode && !ctx"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss index 1d088ec482..5dafcc7521 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss @@ -17,7 +17,6 @@ @import '../../../../../../scss/constants'; :host { - width: 100%; height: 100%; display: flex; flex-direction: column; @@ -25,12 +24,14 @@ } .tb-title { + padding-bottom: 12px; align-self: start; font-weight: 600; font-size: 20px; line-height: 24px; letter-spacing: 0.1px; color: rgba(0, 0, 0, 0.76); + @media #{$mat-md-lg} { font-weight: 500; font-size: 14px; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts index c9c8541613..7296b47f80 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts @@ -18,13 +18,14 @@ import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, OnDestr import { PageComponent } from '@shared/components/page.component'; import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; -import { BadgePosition, BadgeStyle, badgeStyleURLMap, MobileAppSettings } from '@shared/models/mobile-app.models'; +import { BadgePosition, BadgeStyle, badgeStyleURLMap, MobileAppQRCodeSettings } from '@shared/models/mobile-app.models'; import { MobileAppService } from '@core/http/mobile-app.service'; import { WidgetContext } from '@home/models/widget-component.models'; import { UtilsService } from '@core/services/utils.service'; import { interval, mergeMap, Observable, Subject, takeUntil } from 'rxjs'; import { MINUTE } from '@shared/models/time/time.models'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { MobileAppQrCodeWidgetSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widge.models'; @Component({ selector: 'tb-mobile-app-qrcode-widget', @@ -41,7 +42,7 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI previewMode: boolean; @Input() - set mobileAppSettings(settings: MobileAppSettings) { + set mobileAppSettings(settings: MobileAppQRCodeSettings | MobileAppQrCodeWidgetSettings) { if (settings) { this.mobileAppSettingsValue = settings; } @@ -59,7 +60,7 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI badgePosition = BadgePosition; badgeStyleURLMap = badgeStyleURLMap; - private mobileAppSettingsValue: MobileAppSettings; + private mobileAppSettingsValue: MobileAppQRCodeSettings | MobileAppQrCodeWidgetSettings; private deepLinkTTL: number; constructor(protected store: Store, @@ -71,9 +72,8 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI ngOnInit(): void { if (this.ctx) { - this.ctx.$scope.mobileAppQrcodeWidget = this; - } - if (!this.mobileAppSettings) { + this.mobileAppSettings = this.ctx.settings; + } else { this.mobileAppService.getMobileAppSettings().subscribe((settings => { this.mobileAppSettings = settings; this.cd.detectChanges(); @@ -104,7 +104,7 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI updateQRCode(link: string) { import('qrcode').then((QRCode) => { - QRCode.toCanvas(this.canvasRef.nativeElement, link, { width: 125}); + QRCode.toCanvas(this.canvasRef.nativeElement, link, { width: 90 }); }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html new file mode 100644 index 0000000000..cdef956141 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html @@ -0,0 +1,130 @@ + +
+
+
+
admin.mobile-app.applications
+ + {{ 'admin.mobile-app.default' | translate }} + {{ 'admin.mobile-app.custom' | translate }} + +
+
+
+ + {{ 'admin.mobile-app.android' | translate }} + +
+
+
+
{{ 'admin.mobile-app.app-package-name' | translate }}
+ + + + warning + + +
+
+
+
+
{{ 'admin.mobile-app.sha256-certificate-fingerprints' | translate }}
+ + + + warning + + +
+
+
+
+
+ + {{ 'admin.mobile-app.ios' | translate }} + +
+
+
+
{{ 'admin.mobile-app.app-id' | translate }}
+ + + + warning + + +
+
+
+
+
+
+
+ + {{ 'admin.mobile-app.badges' | translate }} + + + + + {{ badgeStyle.value | translate }} + + + + + + + {{ badgePosition.value | translate }} + + + +
+
+
+
+ + {{ 'admin.mobile-app.label' | translate }} + + + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.scss new file mode 100644 index 0000000000..d0a2c9ac16 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.scss @@ -0,0 +1,19 @@ +/** + * 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. + */ + +.tb-fixed-width { + min-width: 230px; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts new file mode 100644 index 0000000000..61999d7d41 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts @@ -0,0 +1,156 @@ +/// +/// 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. +/// + +import { Component } from "@angular/core"; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms"; +import { WidgetSettings, WidgetSettingsComponent } from "@shared/models/widget.models"; +import { AppState } from '@core/core.state'; +import { Store } from "@ngrx/store"; +import { badgePositionTranslationsMap, badgeStyleTranslationsMap } from '@shared/models/mobile-app.models'; +import { mobileAppQrCodeWidgetDefaultSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widge.models'; + +@Component({ + selector: 'tb-mobile-app-qr-code-widget-settings', + templateUrl: './mobile-app-qr-code-widget-settings.component.html', + styleUrls: ['/mobile-app-qr-code-widget-settings.component.scss', './../widget-settings.scss'] +}) +export class MobileAppQrCodeWidgetSettingsComponent extends WidgetSettingsComponent { + + mobileAppQRCodeWidgetSettingsForm: UntypedFormGroup; + + badgePositionTranslationsMap = badgePositionTranslationsMap; + badgeStyleTranslationsMap = badgeStyleTranslationsMap; + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.mobileAppQRCodeWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...mobileAppQrCodeWidgetDefaultSettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.mobileAppQRCodeWidgetSettingsForm = this.fb.group({ + useDefaultApp: [settings.useDefaultApp, []], + androidConfig: this.fb.group({ + enabled: [settings.androidConfig.enabled, []], + appPackage: [settings.androidConfig.appPackage, []], + sha256CertFingerprints: [settings.androidConfig.sha256CertFingerprints, []] + }), + iosConfig: this.fb.group({ + enabled: [settings.iosConfig.enabled, []], + appId: [settings.iosConfig.appId, []] + }), + qrCodeConfig: this.fb.group({ + badgeEnabled: [settings.qrCodeConfig.badgeEnabled, []], + badgeStyle: [{value: settings.qrCodeConfig.badgeStyle, disabled: true}, []], + badgePosition: [{value: settings.qrCodeConfig.badgePosition, disabled: true}, []], + qrCodeLabelEnabled: [settings.qrCodeConfig.qrCodeLabelEnabled, []], + qrCodeLabel: [settings.qrCodeConfig.qrCodeLabel, []] + }) + }); + } + + protected validatorTriggers(): string[] { + return ['useDefaultApp', 'androidConfig.enabled', 'iosConfig.enabled', 'qrCodeConfig.badgeEnabled', 'qrCodeConfig.qrCodeLabelEnabled']; + } + + protected updateValidators(emitEvent: boolean) { + const useDefaultApp = this.mobileAppQRCodeWidgetSettingsForm.get('useDefaultApp').value; + const androidEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.enabled').value; + const iosEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.enabled').value; + const badgeEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').value; + const qrCodeLabelEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').value; + + if (useDefaultApp) { + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').clearValidators(); + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators(); + this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').clearValidators(); + } else { + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]); + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]); + this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').setValidators([Validators.required]); + } + + if (androidEnabled) { + if (!useDefaultApp) { + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]); + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]); + } + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); + if (badgeEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').enable(); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').enable(); + } + } else { + if (!useDefaultApp) { + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').clearValidators(); + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators(); + } + if (!iosEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable(); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable(); + } + } + + if (iosEnabled) { + if (!useDefaultApp) { + this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').setValidators([Validators.required]); + } + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); + if (badgeEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').enable(); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').enable(); + } + } else { + if (!useDefaultApp) { + this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').clearValidators(); + } + if (!androidEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable(); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable(); + } + } + + if (badgeEnabled) { + if (androidEnabled || iosEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').enable(); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').enable(); + } + } else { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable(); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable(); + } + + if (qrCodeLabelEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').enable(); + } else { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').disable(); + } + + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').updateValueAndValidity({emitEvent}); + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').updateValueAndValidity({emitEvent}); + this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').updateValueAndValidity({emitEvent}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index c71f0fd658..f093c4c568 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -354,10 +354,14 @@ import { import { RadarChartWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/radar-chart-widget-settings.component'; +import { + MobileAppQrCodeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component'; @NgModule({ declarations: [ QrCodeWidgetSettingsComponent, + MobileAppQrCodeWidgetSettingsComponent, TimeseriesTableWidgetSettingsComponent, TimeseriesTableKeySettingsComponent, TimeseriesTableLatestKeySettingsComponent, @@ -490,6 +494,7 @@ import { ], exports: [ QrCodeWidgetSettingsComponent, + MobileAppQrCodeWidgetSettingsComponent, TimeseriesTableWidgetSettingsComponent, TimeseriesTableKeySettingsComponent, TimeseriesTableLatestKeySettingsComponent, @@ -620,6 +625,7 @@ export class WidgetSettingsModule { export const widgetSettingsComponentsMap: {[key: string]: Type} = { 'tb-qrcode-widget-settings': QrCodeWidgetSettingsComponent, + 'tb-mobile-app-qr-code-widget-settings': MobileAppQrCodeWidgetSettingsComponent, 'tb-timeseries-table-widget-settings': TimeseriesTableWidgetSettingsComponent, 'tb-timeseries-table-key-settings': TimeseriesTableKeySettingsComponent, 'tb-timeseries-table-latest-key-settings': TimeseriesTableLatestKeySettingsComponent, diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts index c342b68d1f..aa2aca3cb9 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts @@ -27,7 +27,7 @@ import { badgePositionTranslationsMap, BadgeStyle, badgeStyleTranslationsMap, - MobileAppSettings + MobileAppQRCodeSettings } from '@shared/models/mobile-app.models'; @Component({ @@ -39,7 +39,7 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf mobileAppSettingsForm: FormGroup; - mobileAppSettings: MobileAppSettings; + mobileAppSettings: MobileAppQRCodeSettings; private readonly destroy$ = new Subject(); @@ -179,7 +179,7 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf }); } - private processMobileAppSettings(mobileAppSettings: MobileAppSettings): void { + private processMobileAppSettings(mobileAppSettings: MobileAppQRCodeSettings): void { this.mobileAppSettings = {...mobileAppSettings}; this.mobileAppSettingsForm.reset(this.mobileAppSettings); } diff --git a/ui-ngx/src/app/shared/models/mobile-app.models.ts b/ui-ngx/src/app/shared/models/mobile-app.models.ts index 0787f17747..02139bd913 100644 --- a/ui-ngx/src/app/shared/models/mobile-app.models.ts +++ b/ui-ngx/src/app/shared/models/mobile-app.models.ts @@ -16,7 +16,7 @@ import { TenantId } from '@shared/models/id/tenant-id'; -export interface MobileAppSettings { +export interface MobileAppQRCodeSettings { tenantId: TenantId; useDefaultApp: boolean; androidConfig: AndroidConfig; diff --git a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json index d850e640b7..e69b43b1fe 100644 --- a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json @@ -1145,7 +1145,7 @@ "id": "8e71a398-caf5-540d-cec5-6e5dc264343e" }, "b9d704a2-f579-dbff-f123-f953d92081fd": { - "typeFullFqn": "system.home_page_widgets.mobile_app_qr_code", + "typeFullFqn": "system.home_page_widgets.home_mobile_app_qr_code", "type": "static", "sizeX": 6, "sizeY": 3, @@ -1176,12 +1176,10 @@ "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", "padding": "8px", - "settings": { - "cardHtml": "
HTML code here
", - "cardCss": ".card {\n font-weight: bold;\n font-size: 32px;\n color: #999;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n}" - }, - "title": "HTML Card", - "dropShadow": false + "settings": {}, + "title": "Mobile app QR code", + "dropShadow": false, + "enableFullscreen": false }, "row": 0, "col": 0, diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index a692994144..fa1eab0ece 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -290,9 +290,6 @@ &.shrink { flex: 0; } - &.no-gap { - gap: 0; - } } .tb-form-panel, .tb-form-row { From 012457e0db9801fd023d9aac28195985573ab5e8 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 7 May 2024 11:55:06 +0300 Subject: [PATCH 11/13] Add mobileQrEnabled to system params --- .../server/controller/SystemInfoController.java | 10 ++++++++++ .../thingsboard/server/common/data/SystemParams.java | 1 + 2 files changed, 11 insertions(+) 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 cb2033b29d..bc12171306 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -34,10 +34,13 @@ import org.thingsboard.server.common.data.SystemParams; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.common.data.mobile.QRCodeConfig; import org.thingsboard.server.common.data.page.PageLink; 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.MobileAppSettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -45,6 +48,7 @@ import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @Hidden @@ -72,6 +76,9 @@ public class SystemInfoController extends BaseController { @Autowired private EntitiesVersionControlService versionControlService; + @Autowired + private MobileAppSettingsService mobileAppSettingsService; + @PostConstruct public void init() { JsonNode info = buildInfoObject(); @@ -135,6 +142,9 @@ public class SystemInfoController extends BaseController { DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration(); systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize()); } + systemParams.setMobileQrEnabled(Optional.ofNullable(mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID)) + .map(MobileAppSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage) + .orElse(false)); return systemParams; } 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 e022e56c5b..5b4312f7b0 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 @@ -31,4 +31,5 @@ public class SystemParams { JsonNode userSettings; long maxDatapointsLimit; long maxResourceSize; + boolean mobileQrEnabled; } From 0d7d9b481bd93008f44c022c5927c98a4d4e4fa2 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 7 May 2024 12:09:36 +0300 Subject: [PATCH 12/13] Refactoring for mobile settings api --- .../main/data/upgrade/3.6.4/schema_update.sql | 5 +++-- .../MobileApplicationController.java | 7 ++++--- .../secret}/MobileAppSecretService.java | 2 +- .../secret}/MobileAppSecretServiceImpl.java | 2 +- .../secret}/MobileSecretCaffeineCache.java | 2 +- .../secret}/MobileSecretEvictEvent.java | 2 +- .../secret}/MobileSecretRedisCache.java | 2 +- .../mobile/BaseMobileAppSettingsService.java | 20 ++++++++----------- .../mobile/MobileAppSettingsRedisCache.java | 2 +- .../model/sql/MobileAppSettingsEntity.java | 9 +++++---- .../main/resources/sql/schema-entities.sql | 7 ++++--- 11 files changed, 30 insertions(+), 30 deletions(-) rename application/src/main/java/org/thingsboard/server/service/{qr => mobile/secret}/MobileAppSecretService.java (94%) rename application/src/main/java/org/thingsboard/server/service/{qr => mobile/secret}/MobileAppSecretServiceImpl.java (98%) rename application/src/main/java/org/thingsboard/server/service/{qr => mobile/secret}/MobileSecretCaffeineCache.java (96%) rename application/src/main/java/org/thingsboard/server/service/{qr => mobile/secret}/MobileSecretEvictEvent.java (93%) rename application/src/main/java/org/thingsboard/server/service/{qr => mobile/secret}/MobileSecretRedisCache.java (96%) 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 287991ffd1..9a5204379c 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 @@ -157,11 +157,12 @@ DELETE FROM asset_profile WHERE name ='TbServiceQueue'; CREATE TABLE IF NOT EXISTS mobile_app_settings ( id uuid NOT NULL CONSTRAINT mobile_app_settings_pkey PRIMARY KEY, created_time bigint NOT NULL, - tenant_id uuid UNIQUE NOT NULL, + tenant_id uuid NOT NULL, use_default_app boolean, android_config VARCHAR(1000), ios_config VARCHAR(1000), - qr_code_config VARCHAR(100000) + qr_code_config VARCHAR(100000), + CONSTRAINT mobile_app_settings_tenant_id_unq_key UNIQUE (tenant_id) ); -- MOBILE APP SETTINGS TABLE CREATE END \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java index ddd49ab975..626f6f3b0f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java @@ -39,7 +39,7 @@ 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.MobileAppSecretService; +import org.thingsboard.server.service.mobile.secret.MobileAppSecretService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -84,7 +84,7 @@ public class MobileApplicationController extends BaseController { public static final String ANDROID_APPLICATION_STORE_LINK = "https://play.google.com/store/apps/details?id=org.thingsboard.demo.app"; public static final String APPLE_APPLICATION_STORE_LINK = "https://apps.apple.com/us/app/thingsboard-live/id1594355695"; 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 SECRET_PARAM_DESCRIPTION = "A string value representing short-lived 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&ttl=%s"; @@ -164,7 +164,7 @@ public class MobileApplicationController extends BaseController { notes = "Returns the token of the User based on the provided secret key.") @GetMapping(value = "/api/noauth/qr/{secret}") public JwtPair getUserTokenByMobileSecret(@Parameter(description = SECRET_PARAM_DESCRIPTION) - @PathVariable(SECRET) String secret) throws ThingsboardException { + @PathVariable(SECRET) String secret) throws ThingsboardException { checkParameter(SECRET, secret); return mobileAppSecretService.getJwtPair(secret); } @@ -184,4 +184,5 @@ public class MobileApplicationController extends BaseController { .build(); } } + } diff --git a/application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretService.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretService.java similarity index 94% rename from application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretService.java rename to application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretService.java index 943fd45c54..83c1b55c6e 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretService.java +++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretService.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.qr; +package org.thingsboard.server.service.mobile.secret; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.security.model.JwtPair; diff --git a/application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java similarity index 98% rename from application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretServiceImpl.java rename to application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java index f8d8629d10..bef31622ff 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.qr; +package org.thingsboard.server.service.mobile.secret; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretCaffeineCache.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretCaffeineCache.java similarity index 96% rename from application/src/main/java/org/thingsboard/server/service/qr/MobileSecretCaffeineCache.java rename to application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretCaffeineCache.java index 916d58afd7..ce21041d27 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretCaffeineCache.java +++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretCaffeineCache.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.qr; +package org.thingsboard.server.service.mobile.secret; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; diff --git a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretEvictEvent.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretEvictEvent.java similarity index 93% rename from application/src/main/java/org/thingsboard/server/service/qr/MobileSecretEvictEvent.java rename to application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretEvictEvent.java index 1f97e9ab07..b015291d78 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretEvictEvent.java +++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretEvictEvent.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.qr; +package org.thingsboard.server.service.mobile.secret; import lombok.Data; diff --git a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretRedisCache.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java similarity index 96% rename from application/src/main/java/org/thingsboard/server/service/qr/MobileSecretRedisCache.java rename to application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java index d29a104ae9..81fa8672f8 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretRedisCache.java +++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.qr; +package org.thingsboard.server.service.mobile.secret; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.connection.RedisConnectionFactory; 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 index 86de46fed1..4e8a2cd503 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java @@ -17,7 +17,6 @@ package org.thingsboard.server.dao.mobile; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.hibernate.exception.ConstraintViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.id.TenantId; @@ -28,9 +27,10 @@ import org.thingsboard.server.common.data.mobile.IosConfig; import org.thingsboard.server.common.data.mobile.MobileAppSettings; import org.thingsboard.server.common.data.mobile.QRCodeConfig; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; +import java.util.Map; + import static org.thingsboard.server.dao.service.Validator.validateId; @Service @@ -51,16 +51,12 @@ public class BaseMobileAppSettingsService extends AbstractCachedEntityService { private JsonNode qrCodeConfig; public MobileAppSettingsEntity(MobileAppSettings mobileAppSettings) { - if (mobileAppSettings.getId() != null) { - this.setId(mobileAppSettings.getId().getId()); - } + this.setId(mobileAppSettings.getUuidId()); this.setCreatedTime(mobileAppSettings.getCreatedTime()); this.tenantId = mobileAppSettings.getTenantId().getId(); this.useDefaultApp = mobileAppSettings.isUseDefaultApp(); this.androidConfig = toJson(mobileAppSettings.getAndroidConfig()); this.iosConfig = toJson(mobileAppSettings.getIosConfig()); this.qrCodeConfig = toJson(mobileAppSettings.getQrCodeConfig()); - } + } @Override public MobileAppSettings toData() { @@ -81,4 +81,5 @@ public class MobileAppSettingsEntity extends BaseSqlEntity { mobileAppSettings.setQrCodeConfig(fromJson(qrCodeConfig, QRCodeConfig.class)); return mobileAppSettings; } + } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 3a96580011..f2978b7f4a 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -900,9 +900,10 @@ CREATE TABLE IF NOT EXISTS queue_stats ( CREATE TABLE IF NOT EXISTS mobile_app_settings ( id uuid NOT NULL CONSTRAINT mobile_app_settings_pkey PRIMARY KEY, created_time bigint NOT NULL, - tenant_id uuid UNIQUE NOT NULL, + tenant_id uuid NOT NULL, use_default_app boolean, android_config VARCHAR(1000), ios_config VARCHAR(1000), - qr_code_config VARCHAR(100000) -); \ No newline at end of file + qr_code_config VARCHAR(100000), + CONSTRAINT mobile_app_settings_tenant_id_unq_key UNIQUE (tenant_id) +); From d163b5679c3d2cbef72fa521aa5f6c8fede6a1d9 Mon Sep 17 00:00:00 2001 From: rusikv Date: Tue, 7 May 2024 17:14:31 +0300 Subject: [PATCH 13/13] UI: mobile qrcode improvements --- ...ts => mobile-app-qr-code-widget.models.ts} | 0 .../mobile-app-qrcode-widget.component.html | 3 +- .../mobile-app-qrcode-widget.component.scss | 2 +- .../lib/mobile-app-qrcode-widget.component.ts | 7 ++- ...e-app-qr-code-widget-settings.component.ts | 2 +- .../admin/mobile-app-settings.component.ts | 3 +- .../android_store_en_black_badge.svg | 49 ++++++++++++------- .../dashboard/tenant_admin_home_page.json | 4 +- 8 files changed, 45 insertions(+), 25 deletions(-) rename ui-ngx/src/app/modules/home/components/widget/lib/cards/{mobile-app-qr-code-widge.models.ts => mobile-app-qr-code-widget.models.ts} (100%) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widge.models.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html index e3f9ca11c3..35092807dc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html @@ -20,9 +20,10 @@
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss index 5dafcc7521..6d229abb0a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss @@ -51,7 +51,7 @@ } .tb-badge-container { - @media #{$mat-lt-lg} { + @media #{$mat-md} { display: none; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts index 7296b47f80..c7557c75c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts @@ -25,7 +25,7 @@ import { UtilsService } from '@core/services/utils.service'; import { interval, mergeMap, Observable, Subject, takeUntil } from 'rxjs'; import { MINUTE } from '@shared/models/time/time.models'; import { coerceBoolean } from '@shared/decorators/coercion'; -import { MobileAppQrCodeWidgetSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widge.models'; +import { MobileAppQrCodeWidgetSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widget.models'; @Component({ selector: 'tb-mobile-app-qrcode-widget', @@ -104,7 +104,10 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI updateQRCode(link: string) { import('qrcode').then((QRCode) => { - QRCode.toCanvas(this.canvasRef.nativeElement, link, { width: 90 }); + QRCode.toCanvas(this.canvasRef.nativeElement, link, { width: 100 }); + // QRCode.toCanvas(this.canvasRef.nativeElement, link); + // this.canvasRef.nativeElement.style.width = '4.6vw'; + // this.canvasRef.nativeElement.style.height = '4.6vw'; }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts index 61999d7d41..662a9d3dd3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts @@ -20,7 +20,7 @@ import { WidgetSettings, WidgetSettingsComponent } from "@shared/models/widget.m import { AppState } from '@core/core.state'; import { Store } from "@ngrx/store"; import { badgePositionTranslationsMap, badgeStyleTranslationsMap } from '@shared/models/mobile-app.models'; -import { mobileAppQrCodeWidgetDefaultSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widge.models'; +import { mobileAppQrCodeWidgetDefaultSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widget.models'; @Component({ selector: 'tb-mobile-app-qr-code-widget-settings', diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts index aa2aca3cb9..7959b1b800 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts @@ -29,6 +29,7 @@ import { badgeStyleTranslationsMap, MobileAppQRCodeSettings } from '@shared/models/mobile-app.models'; +import { AuthService } from '@core/auth/auth.service'; @Component({ selector: 'tb-mobile-app-settings', @@ -48,7 +49,7 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf constructor(protected store: Store, private mobileAppService: MobileAppService, - public fb: FormBuilder) { + private fb: FormBuilder) { super(store); this.buildMobileAppSettingsForm(); this.mobileAppService.getMobileAppSettings() diff --git a/ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg b/ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg index 02b88dab6f..2ef3c8fc10 100644 --- a/ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg +++ b/ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg @@ -1,22 +1,35 @@ - - - - - - - - - - - - - - - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json index e69b43b1fe..95e33e211b 100644 --- a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json @@ -1258,7 +1258,9 @@ "sizeX": 35, "sizeY": 16, "row": 42, - "col": 85 + "col": 85, + "mobileOrder": 5, + "mobileHeight": 7 } }, "gridSettings": {