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