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),