moved 'enabled' from StoreInfo to QrCoodeSettings

This commit is contained in:
dashevchenko 2024-10-25 18:39:28 +03:00
parent c6396e4074
commit d7582223f4
14 changed files with 132 additions and 71 deletions

View File

@ -41,6 +41,7 @@ CREATE TABLE IF NOT EXISTS mobile_app_bundle (
CONSTRAINT fk_android_app_id FOREIGN KEY (android_app_id) REFERENCES mobile_app(id), CONSTRAINT fk_android_app_id FOREIGN KEY (android_app_id) REFERENCES mobile_app(id),
CONSTRAINT fk_ios_app_id FOREIGN KEY (ios_app_id) REFERENCES mobile_app(id) CONSTRAINT fk_ios_app_id FOREIGN KEY (ios_app_id) REFERENCES mobile_app(id)
); );
CREATE INDEX IF NOT EXISTS mobile_app_bundle_tenant_id ON mobile_app_bundle(tenant_id);
ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS platform_type varchar(32), ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS platform_type varchar(32),
ADD COLUMN IF NOT EXISTS status varchar(32), ADD COLUMN IF NOT EXISTS status varchar(32),
@ -82,31 +83,34 @@ $$
-- duplicate app for iOS platform type -- duplicate app for iOS platform type
iosAppId := uuid_generate_v4(); iosAppId := uuid_generate_v4();
INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, app_secret, platform_type, status) INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, app_secret, platform_type, status)
VALUES (iosAppId, (extract(epoch from now()) * 1000), mobileAppRecord.tenant_id, mobileAppRecord.pkg_name, mobileAppRecord.app_secret, 'IOS', mobileAppRecord.status) VALUES (iosAppId, mobileAppRecord.created_time, mobileAppRecord.tenant_id, mobileAppRecord.pkg_name, mobileAppRecord.app_secret, 'IOS', mobileAppRecord.status)
ON CONFLICT DO NOTHING; ON CONFLICT DO NOTHING;
-- create bundle for android and iOS app -- create bundle for android and iOS app
generatedBundleId := uuid_generate_v4(); generatedBundleId := uuid_generate_v4();
INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id, ios_app_id, oauth2_enabled) INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id, ios_app_id, oauth2_enabled)
VALUES (generatedBundleId, (extract(epoch from now()) * 1000), mobileAppRecord.tenant_id, VALUES (generatedBundleId, mobileAppRecord.created_time, mobileAppRecord.tenant_id,
'Autogenerated for ' || mobileAppRecord.pkg_name, mobileAppRecord.id, iosAppId, mobileAppRecord.oauth2_enabled) mobileAppRecord.pkg_name || ' (autogenerated)', mobileAppRecord.id, iosAppId, mobileAppRecord.oauth2_enabled)
ON CONFLICT DO NOTHING; ON CONFLICT DO NOTHING;
UPDATE mobile_app_bundle_oauth2_client SET mobile_app_bundle_id = generatedBundleId WHERE mobile_app_bundle_id = mobileAppRecord.id; UPDATE mobile_app_bundle_oauth2_client SET mobile_app_bundle_id = generatedBundleId WHERE mobile_app_bundle_id = mobileAppRecord.id;
END LOOP; END LOOP;
END IF; END IF;
ALTER TABLE mobile_app DROP COLUMN IF EXISTS oauth2_enabled; ALTER TABLE mobile_app DROP COLUMN IF EXISTS oauth2_enabled;
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'pkg_name_platform_unique') THEN IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'mobile_app_pkg_name_platform_unq_key') THEN
ALTER TABLE mobile_app ADD CONSTRAINT pkg_name_platform_unique UNIQUE (pkg_name, platform_type); ALTER TABLE mobile_app ADD CONSTRAINT mobile_app_pkg_name_platform_unq_key UNIQUE (pkg_name, platform_type);
END IF; END IF;
END; END;
$$; $$;
ALTER TABLE IF EXISTS mobile_app_settings RENAME TO qr_code_settings; ALTER TABLE IF EXISTS mobile_app_settings RENAME TO qr_code_settings;
ALTER TABLE qr_code_settings ADD COLUMN IF NOT EXISTS mobile_app_bundle_id uuid; ALTER TABLE qr_code_settings ADD COLUMN IF NOT EXISTS mobile_app_bundle_id uuid,
ADD COLUMN IF NOT EXISTS android_enabled boolean,
ADD COLUMN IF NOT EXISTS ios_enabled boolean;
-- migrate mobile apps from qr code settings to mobile_app, create mobile app bundle for the pair of apps -- migrate mobile apps from qr code settings to mobile_app, create mobile app bundle for the pair of apps
DO DO
$$ $$
DECLARE DECLARE
androidPkgName varchar;
iosPkgName varchar; iosPkgName varchar;
androidAppId uuid; androidAppId uuid;
iosAppId uuid; iosAppId uuid;
@ -119,22 +123,28 @@ $$
LOOP LOOP
generatedBundleId := NULL; generatedBundleId := NULL;
-- migrate android config -- migrate android config
SELECT id into androidAppId FROM mobile_app WHERE pkg_name = qrCodeRecord.android_config::jsonb ->> 'appPackage' AND platform_type = 'ANDROID'; IF (qrCodeRecord.android_config IS NOT NULL AND qrCodeRecord.android_config::jsonb -> 'appPackage' IS NOT NULL) THEN
androidPkgName := qrCodeRecord.android_config::jsonb ->> 'appPackage';
SELECT id into androidAppId FROM mobile_app WHERE pkg_name = androidPkgName AND platform_type = 'ANDROID';
IF androidAppId IS NULL THEN IF androidAppId IS NULL THEN
androidAppId := uuid_generate_v4(); androidAppId := uuid_generate_v4();
INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, store_info) INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, store_info)
VALUES (androidAppId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, VALUES (androidAppId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id,
qrCodeRecord.android_config::jsonb ->> 'appPackage', 'ANDROID', 'DRAFT', qrCodeRecord.android_config::jsonb - 'appPackage'); androidPkgName, 'ANDROID', 'DRAFT', qrCodeRecord.android_config::jsonb - 'appPackage' - 'enabled');
generatedBundleId := uuid_generate_v4(); generatedBundleId := uuid_generate_v4();
INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id) INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id)
VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, 'Autogenerated for qr code', androidAppId); VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, androidPkgName || ' (autogenerated)', androidAppId);
UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId WHERE id = qrCodeRecord.id; UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId,
android_enabled = (qrCodeRecord.android_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id;
ELSE ELSE
UPDATE mobile_app SET store_info = qrCodeRecord.android_config::jsonb - 'appPackage' WHERE id = androidAppId; UPDATE mobile_app SET store_info = qrCodeRecord.android_config::jsonb - 'appPackage' - 'enabled' WHERE id = androidAppId;
UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.android_app_id = androidAppId) WHERE id = qrCodeRecord.id; UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.android_app_id = androidAppId),
android_enabled = (qrCodeRecord.android_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id;
END IF;
END IF; END IF;
-- migrate ios config -- migrate ios config
IF (qrCodeRecord.ios_config IS NOT NULL AND qrCodeRecord.ios_config::jsonb -> 'appId' IS NOT NULL) THEN
iosPkgName := substring(qrCodeRecord.ios_config::jsonb ->> 'appId', strpos(qrCodeRecord.ios_config::jsonb ->> 'appId', '.') + 1); iosPkgName := substring(qrCodeRecord.ios_config::jsonb ->> 'appId', strpos(qrCodeRecord.ios_config::jsonb ->> 'appId', '.') + 1);
SELECT id INTO iosAppId FROM mobile_app WHERE pkg_name = iosPkgName AND platform_type = 'IOS'; SELECT id INTO iosAppId FROM mobile_app WHERE pkg_name = iosPkgName AND platform_type = 'IOS';
IF iosAppId IS NULL THEN IF iosAppId IS NULL THEN
@ -145,14 +155,17 @@ $$
IF generatedBundleId IS NULL THEN IF generatedBundleId IS NULL THEN
generatedBundleId := uuid_generate_v4(); generatedBundleId := uuid_generate_v4();
INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, ios_app_id) INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, ios_app_id)
VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, 'Autogenerated for qr code', iosAppId); VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, iosPkgName || ' (autogenerated)', iosAppId);
UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId WHERE id = qrCodeRecord.id; UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId,
ios_enabled = (qrCodeRecord.ios_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id;
ELSE ELSE
UPDATE mobile_app_bundle SET ios_app_id = iosAppId WHERE id = generatedBundleId; UPDATE mobile_app_bundle SET ios_app_id = iosAppId WHERE id = generatedBundleId;
END IF; END IF;
ELSE ELSE
UPDATE mobile_app SET store_info = qrCodeRecord.ios_config WHERE id = iosAppId; UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.ios_app_id = iosAppId),
UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.ios_app_id = iosAppId) WHERE id = qrCodeRecord.id; ios_enabled = (qrCodeRecord.ios_config::jsonb -> 'enabled')::boolean WHERE id = qrCodeRecord.id;
UPDATE mobile_app SET store_info = qrCodeRecord.ios_config::jsonb - 'enabled' WHERE id = iosAppId;
END IF;
END IF; END IF;
END LOOP; END LOOP;
ALTER TABLE qr_code_settings RENAME CONSTRAINT mobile_app_settings_tenant_id_unq_key TO qr_code_settings_tenant_id_unq_key; ALTER TABLE qr_code_settings RENAME CONSTRAINT mobile_app_settings_tenant_id_unq_key TO qr_code_settings_tenant_id_unq_key;

View File

@ -95,14 +95,13 @@ public class QrCodeSettingsController extends BaseController {
private final SystemSecurityService systemSecurityService; private final SystemSecurityService systemSecurityService;
private final MobileAppSecretService mobileAppSecretService; private final MobileAppSecretService mobileAppSecretService;
private final QrCodeSettingService qrCodeSettingService; private final QrCodeSettingService qrCodeSettingService;
private final MobileAppService mobileAppService;
@ApiOperation(value = "Get associated android applications (getAssetLinks)") @ApiOperation(value = "Get associated android applications (getAssetLinks)")
@GetMapping(value = "/.well-known/assetlinks.json") @GetMapping(value = "/.well-known/assetlinks.json")
public ResponseEntity<JsonNode> getAssetLinks() { public ResponseEntity<JsonNode> getAssetLinks() {
MobileApp mobileApp = qrCodeSettingService.findAppFromQrCodeSettings(TenantId.SYS_TENANT_ID, ANDROID); MobileApp mobileApp = qrCodeSettingService.findAppFromQrCodeSettings(TenantId.SYS_TENANT_ID, ANDROID);
StoreInfo storeInfo = mobileApp != null ? mobileApp.getStoreInfo() : null; StoreInfo storeInfo = mobileApp != null ? mobileApp.getStoreInfo() : null;
if (storeInfo != null && storeInfo.isEnabled() && storeInfo.getSha256CertFingerprints() != null) { if (storeInfo != null && storeInfo.getSha256CertFingerprints() != null) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, mobileApp.getPkgName(), storeInfo.getSha256CertFingerprints()))); return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, mobileApp.getPkgName(), storeInfo.getSha256CertFingerprints())));
} else { } else {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
@ -114,7 +113,7 @@ public class QrCodeSettingsController extends BaseController {
public ResponseEntity<JsonNode> getAppleAppSiteAssociation() { public ResponseEntity<JsonNode> getAppleAppSiteAssociation() {
MobileApp mobileApp = qrCodeSettingService.findAppFromQrCodeSettings(TenantId.SYS_TENANT_ID, IOS); MobileApp mobileApp = qrCodeSettingService.findAppFromQrCodeSettings(TenantId.SYS_TENANT_ID, IOS);
StoreInfo storeInfo = mobileApp != null ? mobileApp.getStoreInfo() : null; StoreInfo storeInfo = mobileApp != null ? mobileApp.getStoreInfo() : null;
if (storeInfo != null && storeInfo.isEnabled() && storeInfo.getAppId() != null) { if (storeInfo != null && storeInfo.getAppId() != null) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, storeInfo.getAppId()))); return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, storeInfo.getAppId())));
} else { } else {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
@ -177,14 +176,13 @@ public class QrCodeSettingsController extends BaseController {
@GetMapping(value = "/api/noauth/qr") @GetMapping(value = "/api/noauth/qr")
public ResponseEntity<?> getApplicationRedirect(@RequestHeader(value = "User-Agent") String userAgent) { public ResponseEntity<?> getApplicationRedirect(@RequestHeader(value = "User-Agent") String userAgent) {
QrCodeSettings qrCodeSettings = qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID); QrCodeSettings qrCodeSettings = qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID);
boolean useDefaultApp = qrCodeSettings.isUseDefaultApp(); if (userAgent.contains("Android") && qrCodeSettings.isAndroidEnabled()) {
if (userAgent.contains("Android")) { String googlePlayLink = qrCodeSettings.getGooglePlayLink();
String googlePlayLink = useDefaultApp ? qrCodeSettings.getGooglePlayLink() : getStoreLink(qrCodeSettings.getMobileAppBundleId(), ANDROID);
return ResponseEntity.status(HttpStatus.FOUND) return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", googlePlayLink) .header("Location", googlePlayLink)
.build(); .build();
} else if (userAgent.contains("iPhone") || userAgent.contains("iPad")) { } else if (userAgent.contains("iPhone") || userAgent.contains("iPad") && qrCodeSettings.isIosEnabled()) {
String appStoreLink = useDefaultApp ? qrCodeSettings.getAppStoreLink() : getStoreLink(qrCodeSettings.getMobileAppBundleId(), IOS); String appStoreLink = qrCodeSettings.getAppStoreLink();
return ResponseEntity.status(HttpStatus.FOUND) return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", appStoreLink) .header("Location", appStoreLink)
.build(); .build();
@ -193,12 +191,4 @@ public class QrCodeSettingsController extends BaseController {
} }
} }
private String getStoreLink(MobileAppBundleId mobileAppBundleId, PlatformType platformType) {
if (mobileAppBundleId == null) {
return null;
}
MobileApp mobileApp = mobileAppService.findByBundleIdAndPlatformType(TenantId.SYS_TENANT_ID, mobileAppBundleId, platformType);
return (mobileApp != null && mobileApp.getStoreInfo() != null) ? mobileApp.getStoreInfo().getStoreLink() : null;
}
} }

View File

@ -70,7 +70,6 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest {
StoreInfo androidStoreInfo = StoreInfo.builder() StoreInfo androidStoreInfo = StoreInfo.builder()
.sha256CertFingerprints(ANDROID_APP_SHA256) .sha256CertFingerprints(ANDROID_APP_SHA256)
.storeLink(ANDROID_STORE_LINK) .storeLink(ANDROID_STORE_LINK)
.enabled(true)
.build(); .build();
androidApp.setStoreInfo(androidStoreInfo); androidApp.setStoreInfo(androidStoreInfo);
MobileApp savedAndroidApp = doPost("/api/mobile/app", androidApp, MobileApp.class); MobileApp savedAndroidApp = doPost("/api/mobile/app", androidApp, MobileApp.class);
@ -78,7 +77,6 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest {
MobileApp iosApp = validMobileApp( "my.ios.package", PlatformType.IOS); MobileApp iosApp = validMobileApp( "my.ios.package", PlatformType.IOS);
StoreInfo iosQrCodeConfig = StoreInfo.builder() StoreInfo iosQrCodeConfig = StoreInfo.builder()
.appId(APPLE_APP_ID) .appId(APPLE_APP_ID)
.enabled(true)
.storeLink(IOS_STORE_LINK) .storeLink(IOS_STORE_LINK)
.build(); .build();
iosApp.setStoreInfo(iosQrCodeConfig); iosApp.setStoreInfo(iosQrCodeConfig);

View File

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.alarm.EntityAlarm;
@ -45,6 +46,9 @@ import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -54,7 +58,12 @@ import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.mobile.app.MobileAppStatus;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
@ -233,6 +242,27 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
tenantId = differentTenantId; tenantId = differentTenantId;
createRelatedData(tenantId); createRelatedData(tenantId);
MobileApp androidApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.android.package", PlatformType.ANDROID);
androidApp = doPost("/api/mobile/app", androidApp, MobileApp.class);
MobileAppId androidAppId = androidApp.getId();
MobileApp iosApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.ios.package", PlatformType.IOS);
iosApp = doPost("/api/mobile/app", iosApp, MobileApp.class);
MobileAppId iosAppId = androidApp.getId();
OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client");
OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class);
OAuth2ClientId oAuth2ClientId = savedOAuth2Client.getId();
MobileAppBundle mobileAppBundle = new MobileAppBundle();
mobileAppBundle.setTitle("Test bundle");
mobileAppBundle.setAndroidAppId(androidApp.getId());
mobileAppBundle.setIosAppId(iosApp.getId());
MobileAppBundle savedAppBundle = doPost("/api/mobile/bundle?oauth2ClientIds=" + savedOAuth2Client.getId().getId(), mobileAppBundle, MobileAppBundle.class);
MobileAppBundleId appBundleId = savedAppBundle.getId();
createDifferentTenantCustomer(); createDifferentTenantCustomer();
createRelatedData(differentTenantCustomerId); createRelatedData(differentTenantCustomerId);
loginDifferentTenant(); loginDifferentTenant();
@ -279,6 +309,10 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
verifyNoRelatedData(userId); verifyNoRelatedData(userId);
verifyNoRelatedData(differentTenantCustomerId); verifyNoRelatedData(differentTenantCustomerId);
verifyNoRelatedData(tenantApiUsageState.getId()); verifyNoRelatedData(tenantApiUsageState.getId());
verifyNoRelatedData(androidAppId);
verifyNoRelatedData(iosAppId);
verifyNoRelatedData(oAuth2ClientId);
verifyNoRelatedData(appBundleId);
verifyNoRelatedData(tenantId); verifyNoRelatedData(tenantId);
}); });
} }
@ -483,4 +517,14 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainId); return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainId);
} }
private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, PlatformType platformType) {
MobileApp mobileApp = new MobileApp();
mobileApp.setTenantId(tenantId);
mobileApp.setStatus(MobileAppStatus.DRAFT);
mobileApp.setPkgName(mobileAppName);
mobileApp.setPlatformType(platformType);
mobileApp.setAppSecret(StringUtils.randomAlphanumeric(24));
return mobileApp;
}
} }

View File

@ -23,7 +23,6 @@ import org.thingsboard.server.common.data.validation.NoXss;
@Builder @Builder
public class StoreInfo { public class StoreInfo {
private boolean enabled;
@NoXss @NoXss
private String appId; private String appId;
@NoXss @NoXss

View File

@ -44,10 +44,12 @@ public class QrCodeSettings extends BaseData<QrCodeSettingsId> implements HasTen
@Valid @Valid
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "QR code config configuration.") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "QR code config configuration.")
private QRCodeConfig qrCodeConfig; private QRCodeConfig qrCodeConfig;
@Schema(description = "Indicates if google play link is available", example = "true")
private boolean androidEnabled;
@Schema(description = "Indicates if apple store link is available", example = "true")
private boolean iosEnabled;
@JsonProperty(access = JsonProperty.Access.READ_ONLY) @JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String googlePlayLink; private String googlePlayLink;
@JsonProperty(access = JsonProperty.Access.READ_ONLY) @JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String appStoreLink; private String appStoreLink;

View File

@ -56,7 +56,7 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil
return savedMobileApp; return savedMobileApp;
} catch (Exception e) { } catch (Exception e) {
checkConstraintViolation(e, checkConstraintViolation(e,
Map.of("mobile_app_unq_key", "Mobile app with such package already exists!")); Map.of("mobile_app_pkg_name_platform_unq_key", "Mobile app with such package name and platform already exists!"));
throw e; throw e;
} }
} }

View File

@ -119,10 +119,10 @@ public class QrCodeSettingServiceImpl extends AbstractCachedEntityService<Tenant
} else { } else {
MobileApp androidApp = mobileAppService.findByBundleIdAndPlatformType(qrCodeSettings.getTenantId(), qrCodeSettings.getMobileAppBundleId(), ANDROID); MobileApp androidApp = mobileAppService.findByBundleIdAndPlatformType(qrCodeSettings.getTenantId(), qrCodeSettings.getMobileAppBundleId(), ANDROID);
MobileApp iosApp = mobileAppService.findByBundleIdAndPlatformType(qrCodeSettings.getTenantId(), qrCodeSettings.getMobileAppBundleId(), IOS); MobileApp iosApp = mobileAppService.findByBundleIdAndPlatformType(qrCodeSettings.getTenantId(), qrCodeSettings.getMobileAppBundleId(), IOS);
if (androidApp != null && androidApp.getStoreInfo() != null && androidApp.getStoreInfo().isEnabled()) { if (androidApp != null && androidApp.getStoreInfo() != null) {
qrCodeSettings.setGooglePlayLink(androidApp.getStoreInfo().getStoreLink()); qrCodeSettings.setGooglePlayLink(androidApp.getStoreInfo().getStoreLink());
} }
if (iosApp != null && iosApp.getStoreInfo() != null && iosApp.getStoreInfo().isEnabled()) { if (iosApp != null && iosApp.getStoreInfo() != null) {
qrCodeSettings.setAppStoreLink(iosApp.getStoreInfo().getStoreLink()); qrCodeSettings.setAppStoreLink(iosApp.getStoreInfo().getStoreLink());
} }
} }

View File

@ -701,6 +701,8 @@ public class ModelConstants {
*/ */
public static final String QR_CODE_SETTINGS_TABLE_NAME = "qr_code_settings"; public static final String QR_CODE_SETTINGS_TABLE_NAME = "qr_code_settings";
public static final String QR_CODE_SETTINGS_USE_DEFAULT_APP_PROPERTY = "use_default_app"; public static final String QR_CODE_SETTINGS_USE_DEFAULT_APP_PROPERTY = "use_default_app";
public static final String QR_CODE_SETTINGS_ANDROID_ENABLED_PROPERTY = "android_enabled";
public static final String QR_CODE_SETTINGS_IOS_ENABLED_PROPERTY = "ios_enabled";
public static final String QR_CODE_SETTINGS_BUNDLE_ID_PROPERTY = "mobile_app_bundle_id"; public static final String QR_CODE_SETTINGS_BUNDLE_ID_PROPERTY = "mobile_app_bundle_id";
public static final String QR_CODE_SETTINGS_CONFIG_PROPERTY = "qr_code_config"; public static final String QR_CODE_SETTINGS_CONFIG_PROPERTY = "qr_code_config";

View File

@ -47,6 +47,12 @@ public class QrCodeSettingsEntity extends BaseSqlEntity<QrCodeSettings> {
@Column(name = ModelConstants.QR_CODE_SETTINGS_USE_DEFAULT_APP_PROPERTY) @Column(name = ModelConstants.QR_CODE_SETTINGS_USE_DEFAULT_APP_PROPERTY)
private boolean useDefaultApp; private boolean useDefaultApp;
@Column(name = ModelConstants.QR_CODE_SETTINGS_ANDROID_ENABLED_PROPERTY)
private boolean androidEnabled;
@Column(name = ModelConstants.QR_CODE_SETTINGS_IOS_ENABLED_PROPERTY)
private boolean iosEnabled;
@Column(name = ModelConstants.QR_CODE_SETTINGS_BUNDLE_ID_PROPERTY) @Column(name = ModelConstants.QR_CODE_SETTINGS_BUNDLE_ID_PROPERTY)
private UUID mobileAppBundleId; private UUID mobileAppBundleId;
@ -59,6 +65,8 @@ public class QrCodeSettingsEntity extends BaseSqlEntity<QrCodeSettings> {
this.setCreatedTime(qrCodeSettings.getCreatedTime()); this.setCreatedTime(qrCodeSettings.getCreatedTime());
this.tenantId = qrCodeSettings.getTenantId().getId(); this.tenantId = qrCodeSettings.getTenantId().getId();
this.useDefaultApp = qrCodeSettings.isUseDefaultApp(); this.useDefaultApp = qrCodeSettings.isUseDefaultApp();
this.androidEnabled = qrCodeSettings.isAndroidEnabled();
this.iosEnabled = qrCodeSettings.isIosEnabled();
if (qrCodeSettings.getMobileAppBundleId() != null) { if (qrCodeSettings.getMobileAppBundleId() != null) {
this.mobileAppBundleId = qrCodeSettings.getMobileAppBundleId().getId(); this.mobileAppBundleId = qrCodeSettings.getMobileAppBundleId().getId();
} }
@ -71,6 +79,8 @@ public class QrCodeSettingsEntity extends BaseSqlEntity<QrCodeSettings> {
qrCodeSettings.setCreatedTime(createdTime); qrCodeSettings.setCreatedTime(createdTime);
qrCodeSettings.setTenantId(TenantId.fromUUID(tenantId)); qrCodeSettings.setTenantId(TenantId.fromUUID(tenantId));
qrCodeSettings.setUseDefaultApp(useDefaultApp); qrCodeSettings.setUseDefaultApp(useDefaultApp);
qrCodeSettings.setAndroidEnabled(androidEnabled);
qrCodeSettings.setIosEnabled(iosEnabled);
if (mobileAppBundleId != null) { if (mobileAppBundleId != null) {
qrCodeSettings.setMobileAppBundleId(new MobileAppBundleId(mobileAppBundleId)); qrCodeSettings.setMobileAppBundleId(new MobileAppBundleId(mobileAppBundleId));
} }

View File

@ -31,12 +31,12 @@ public class MobileAppDataValidator extends DataValidator<MobileApp> {
@Override @Override
protected void validateDataImpl(TenantId tenantId, MobileApp mobileApp) { protected void validateDataImpl(TenantId tenantId, MobileApp mobileApp) {
if (mobileApp.getPlatformType() == PlatformType.ANDROID) { if (mobileApp.getPlatformType() == PlatformType.ANDROID) {
if (mobileApp.getStoreInfo() != null && mobileApp.getStoreInfo().isEnabled() && if (mobileApp.getStoreInfo() != null &&
(mobileApp.getStoreInfo().getSha256CertFingerprints() == null || mobileApp.getStoreInfo().getStoreLink() == null)) { (mobileApp.getStoreInfo().getSha256CertFingerprints() == null || mobileApp.getStoreInfo().getStoreLink() == null)) {
throw new DataValidationException("Sha256CertFingerprints and store link are required"); throw new DataValidationException("Sha256CertFingerprints and store link are required");
} }
} else if (mobileApp.getPlatformType() == PlatformType.IOS) { } else if (mobileApp.getPlatformType() == PlatformType.IOS) {
if (mobileApp.getStoreInfo() != null && mobileApp.getStoreInfo().isEnabled() && if (mobileApp.getStoreInfo() != null &&
(mobileApp.getStoreInfo().getAppId() == null || mobileApp.getStoreInfo().getStoreLink() == null)) { (mobileApp.getStoreInfo().getAppId() == null || mobileApp.getStoreInfo().getStoreLink() == null)) {
throw new DataValidationException("AppId and store link are required"); throw new DataValidationException("AppId and store link are required");
} }

View File

@ -127,3 +127,5 @@ CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag);
CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag);
CREATE INDEX IF NOT EXISTS idx_resource_type_public_resource_key ON resource(resource_type, public_resource_key); CREATE INDEX IF NOT EXISTS idx_resource_type_public_resource_key ON resource(resource_type, public_resource_key);
CREATE INDEX IF NOT EXISTS mobile_app_bundle_tenant_id ON mobile_app_bundle(tenant_id);

View File

@ -628,7 +628,7 @@ CREATE TABLE IF NOT EXISTS mobile_app (
status varchar(32), status varchar(32),
version_info varchar(100000), version_info varchar(100000),
store_info varchar(16384), store_info varchar(16384),
CONSTRAINT pkg_name_platform_unique UNIQUE (pkg_name, platform_type) CONSTRAINT mobile_app_pkg_name_platform_unq_key UNIQUE (pkg_name, platform_type)
); );
CREATE TABLE IF NOT EXISTS mobile_app_bundle ( CREATE TABLE IF NOT EXISTS mobile_app_bundle (

View File

@ -126,6 +126,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo;
@ -2108,12 +2109,12 @@ public class RestClient implements Closeable {
}, params).getBody(); }, params).getBody();
} }
public List<OAuth2ClientInfo> getTenantOAuth2Clients() { public PageData<OAuth2ClientInfo> getTenantOAuth2Clients() {
return restTemplate.exchange( return restTemplate.exchange(
baseURL + "/api/oauth2/client/infos", baseURL + "/api/oauth2/client/infos",
HttpMethod.GET, HttpMethod.GET,
HttpEntity.EMPTY, HttpEntity.EMPTY,
new ParameterizedTypeReference<List<OAuth2ClientInfo>>() { new ParameterizedTypeReference<PageData<OAuth2ClientInfo>>() {
}).getBody(); }).getBody();
} }
@ -2138,12 +2139,12 @@ public class RestClient implements Closeable {
restTemplate.delete(baseURL + "/api/oauth2/client/{id}", oAuth2ClientId.getId()); restTemplate.delete(baseURL + "/api/oauth2/client/{id}", oAuth2ClientId.getId());
} }
public List<DomainInfo> getTenantDomainInfos() { public PageData<DomainInfo> getTenantDomainInfos() {
return restTemplate.exchange( return restTemplate.exchange(
baseURL + "/api/domain/infos", baseURL + "/api/domain/infos",
HttpMethod.GET, HttpMethod.GET,
HttpEntity.EMPTY, HttpEntity.EMPTY,
new ParameterizedTypeReference<List<DomainInfo>>() { new ParameterizedTypeReference<PageData<DomainInfo>>() {
}).getBody(); }).getBody();
} }
@ -2172,12 +2173,12 @@ public class RestClient implements Closeable {
restTemplate.postForLocation(baseURL + "/api/domain/{id}/oauth2Clients", oauth2ClientIds, domainId.getId()); restTemplate.postForLocation(baseURL + "/api/domain/{id}/oauth2Clients", oauth2ClientIds, domainId.getId());
} }
public List<DomainInfo> getTenantMobileApps() { public PageData<MobileApp> getTenantMobileApps() {
return restTemplate.exchange( return restTemplate.exchange(
baseURL + "/api/mobile/app", baseURL + "/api/mobile/app",
HttpMethod.GET, HttpMethod.GET,
HttpEntity.EMPTY, HttpEntity.EMPTY,
new ParameterizedTypeReference<List<DomainInfo>>() { new ParameterizedTypeReference<PageData<MobileApp>>() {
}).getBody(); }).getBody();
} }
@ -2202,18 +2203,18 @@ public class RestClient implements Closeable {
restTemplate.delete(baseURL + "/api/mobile/app/{id}", mobileAppId.getId()); restTemplate.delete(baseURL + "/api/mobile/app/{id}", mobileAppId.getId());
} }
public List<DomainInfo> getTenantMobileBundleInfos() { public PageData<MobileAppBundleInfo> getTenantMobileBundleInfos() {
return restTemplate.exchange( return restTemplate.exchange(
baseURL + "/api/mobile/bundle/infos", baseURL + "/api/mobile/bundle/infos",
HttpMethod.GET, HttpMethod.GET,
HttpEntity.EMPTY, HttpEntity.EMPTY,
new ParameterizedTypeReference<List<DomainInfo>>() { new ParameterizedTypeReference<PageData<MobileAppBundleInfo>>() {
}).getBody(); }).getBody();
} }
public Optional<MobileAppBundle> getMobileBundleById(MobileAppBundleId mobileAppBundleId) { public Optional<MobileAppBundle> getMobileBundleById(MobileAppBundleId mobileAppBundleId) {
try { try {
ResponseEntity<MobileAppBundle> mobileApp = restTemplate.getForEntity(baseURL + "/api/mobile/app/{id}", MobileAppBundle.class, mobileAppBundleId.getId()); ResponseEntity<MobileAppBundle> mobileApp = restTemplate.getForEntity(baseURL + "/api/mobile/bundle/{id}", MobileAppBundle.class, mobileAppBundleId.getId());
return Optional.ofNullable(mobileApp.getBody()); return Optional.ofNullable(mobileApp.getBody());
} catch (HttpClientErrorException exception) { } catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {