Browse Source

Merge pull request #11835 from thingsboard/feature/mobile-app-bundle

Mobile app customization
pull/12065/head
Andrew Shvayka 2 years ago
committed by GitHub
parent
commit
eab4e0e4ac
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 153
      application/src/main/data/upgrade/3.8.1/schema_update.sql
  2. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  3. 6
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  4. 52
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  5. 36
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  6. 131
      application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java
  7. 143
      application/src/main/java/org/thingsboard/server/controller/MobileAppController.java
  8. 29
      application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
  9. 71
      application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java
  10. 12
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  11. 83
      application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppBundleService.java
  12. 24
      application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java
  13. 32
      application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppBundleService.java
  14. 9
      application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java
  15. 4
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  16. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  17. 5
      application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
  18. 4
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  19. 4
      application/src/main/resources/thingsboard.yml
  20. 169
      application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java
  21. 104
      application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java
  22. 253
      application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java
  23. 262
      application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java
  24. 44
      application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java
  25. 46
      common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java
  26. 19
      common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java
  27. 2
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  28. 3
      common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
  29. 21
      common/data/src/main/java/org/thingsboard/server/common/data/Views.java
  30. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  31. 39
      common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java
  32. 8
      common/data/src/main/java/org/thingsboard/server/common/data/id/QrCodeSettingsId.java
  33. 24
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/LoginMobileInfo.java
  34. 11
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java
  35. 27
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileSessionInfo.java
  36. 24
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileApp.java
  37. 25
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppStatus.java
  38. 49
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppVersionInfo.java
  39. 13
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/StoreInfo.java
  40. 83
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java
  41. 63
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundleInfo.java
  42. 8
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundleOauth2Client.java
  43. 35
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java
  44. 42
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java
  45. 42
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java
  46. 43
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java
  47. 29
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultPageId.java
  48. 23
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java
  49. 45
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java
  50. 24
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePageType.java
  51. 42
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java
  52. 2
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/BadgePosition.java
  53. 3
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QRCodeConfig.java
  54. 37
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QrCodeSettings.java
  55. 1
      common/proto/src/main/proto/queue.proto
  56. 13
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  57. 120
      dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java
  58. 46
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.java
  59. 170
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java
  60. 19
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java
  61. 96
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java
  62. 12
      dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java
  63. 134
      dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java
  64. 10
      dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsCaffeineCache.java
  65. 6
      dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsDao.java
  66. 2
      dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsEvictEvent.java
  67. 10
      dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsRedisCache.java
  68. 33
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  69. 114
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractMobileAppBundleEntity.java
  70. 35
      dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java
  71. 45
      dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleInfoEntity.java
  72. 35
      dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleOauth2ClientEntity.java
  73. 38
      dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java
  74. 2
      dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientCompositeKey.java
  75. 85
      dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java
  76. 91
      dao/src/main/java/org/thingsboard/server/dao/model/sql/QrCodeSettingsEntity.java
  77. 2
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java
  78. 60
      dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppBundleDataValidator.java
  79. 50
      dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppDataValidator.java
  80. 51
      dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java
  81. 59
      dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java
  82. 101
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java
  83. 36
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java
  84. 24
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaQrCodeSettingsDao.java
  85. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleOauth2ClientRepository.java
  86. 70
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java
  87. 11
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppRepository.java
  88. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/QrCodeSettingsRepository.java
  89. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java
  90. 32
      dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java
  91. 8
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  92. 14
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  93. 2
      dao/src/main/resources/sql/schema-entities-idx.sql
  94. 37
      dao/src/main/resources/sql/schema-entities.sql
  95. 48
      dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java
  96. 4
      dao/src/test/resources/application-test.properties
  97. 64
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  98. 3
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  99. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java
  100. 12
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java

153
application/src/main/data/upgrade/3.8.1/schema_update.sql

@ -24,3 +24,156 @@ UPDATE user_credentials c SET failed_login_attempts = (SELECT (additional_info::
UPDATE tb_user SET additional_info = (additional_info::jsonb - 'lastLoginTs' - 'failedLoginAttempts' - 'userCredentialsEnabled')::text
WHERE additional_info IS NOT NULL AND additional_info != 'null';
-- CREATE MOBILE APP BUNDLES FROM EXISTING APPS
CREATE TABLE IF NOT EXISTS mobile_app_bundle (
id uuid NOT NULL CONSTRAINT mobile_app_bundle_pkey PRIMARY KEY,
created_time bigint NOT NULL,
tenant_id uuid,
title varchar(255),
description varchar(1024),
android_app_id uuid UNIQUE,
ios_app_id uuid UNIQUE,
layout_config varchar(16384),
oauth2_enabled boolean,
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)
);
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),
ADD COLUMN IF NOT EXISTS status varchar(32),
ADD COLUMN IF NOT EXISTS version_info varchar(100000),
ADD COLUMN IF NOT EXISTS store_info varchar(16384),
DROP CONSTRAINT IF EXISTS mobile_app_pkg_name_key,
DROP CONSTRAINT IF EXISTS mobile_app_unq_key;
-- rename mobile_app_oauth2_client to mobile_app_bundle_oauth2_client
DO
$$
BEGIN
-- in case of running the upgrade script a second time
IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'mobile_app_oauth2_client') THEN
ALTER TABLE mobile_app_oauth2_client RENAME TO mobile_app_bundle_oauth2_client;
ALTER TABLE mobile_app_bundle_oauth2_client DROP CONSTRAINT IF EXISTS fk_domain;
ALTER TABLE mobile_app_bundle_oauth2_client RENAME COLUMN mobile_app_id TO mobile_app_bundle_id;
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'fk_mobile_app_bundle_oauth2_client_bundle_id') THEN
ALTER TABLE mobile_app_bundle_oauth2_client ADD CONSTRAINT fk_mobile_app_bundle_oauth2_client_bundle_id
FOREIGN KEY (mobile_app_bundle_id) REFERENCES mobile_app_bundle(id) ON DELETE CASCADE;
END IF;
END IF;
END;
$$;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- duplicate each mobile app and create mobile app bundle for the pair of android and ios app
DO
$$
DECLARE
generatedBundleId uuid;
iosAppId uuid;
mobileAppRecord RECORD;
BEGIN
-- in case of running the upgrade script a second time
IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'mobile_app' and column_name = 'oauth2_enabled') THEN
UPDATE mobile_app SET platform_type = 'ANDROID' WHERE platform_type IS NULL;
UPDATE mobile_app SET status = 'DRAFT' WHERE mobile_app.status IS NULL;
FOR mobileAppRecord IN SELECT * FROM mobile_app
LOOP
-- duplicate app for iOS platform type
iosAppId := uuid_generate_v4();
INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, app_secret, platform_type, status)
VALUES (iosAppId, mobileAppRecord.created_time, mobileAppRecord.tenant_id, mobileAppRecord.pkg_name, mobileAppRecord.app_secret, 'IOS', mobileAppRecord.status)
ON CONFLICT DO NOTHING;
-- create bundle for android and iOS app
generatedBundleId := uuid_generate_v4();
INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id, ios_app_id, oauth2_enabled)
VALUES (generatedBundleId, mobileAppRecord.created_time, mobileAppRecord.tenant_id,
mobileAppRecord.pkg_name || ' (autogenerated)', mobileAppRecord.id, iosAppId, mobileAppRecord.oauth2_enabled)
ON CONFLICT DO NOTHING;
UPDATE mobile_app_bundle_oauth2_client SET mobile_app_bundle_id = generatedBundleId WHERE mobile_app_bundle_id = mobileAppRecord.id;
END LOOP;
END IF;
ALTER TABLE mobile_app DROP COLUMN IF EXISTS oauth2_enabled;
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'mobile_app_pkg_name_platform_unq_key') THEN
ALTER TABLE mobile_app ADD CONSTRAINT mobile_app_pkg_name_platform_unq_key UNIQUE (pkg_name, platform_type);
END IF;
END;
$$;
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,
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
DO
$$
DECLARE
androidPkgName varchar;
iosPkgName varchar;
androidAppId uuid;
iosAppId uuid;
generatedBundleId uuid;
qrCodeRecord RECORD;
BEGIN
-- in case of running the upgrade script a second time
IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'qr_code_settings' AND column_name = 'android_config') THEN
FOR qrCodeRecord IN SELECT * FROM qr_code_settings
LOOP
generatedBundleId := NULL;
-- migrate android config
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
androidAppId := uuid_generate_v4();
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,
androidPkgName, 'ANDROID', 'DRAFT', qrCodeRecord.android_config::jsonb - 'appPackage' - 'enabled');
generatedBundleId := uuid_generate_v4();
INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id)
VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, androidPkgName || ' (autogenerated)', androidAppId);
UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId,
android_enabled = (qrCodeRecord.android_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id;
ELSE
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),
android_enabled = (qrCodeRecord.android_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id;
END IF;
END IF;
-- 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);
SELECT id INTO iosAppId FROM mobile_app WHERE pkg_name = iosPkgName AND platform_type = 'IOS';
IF iosAppId IS NULL THEN
iosAppId := uuid_generate_v4();
INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, store_info)
VALUES (iosAppId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id,
iosPkgName, 'IOS', 'DRAFT', qrCodeRecord.ios_config);
IF generatedBundleId IS NULL THEN
generatedBundleId := uuid_generate_v4();
INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, ios_app_id)
VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, iosPkgName || ' (autogenerated)', iosAppId);
UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId,
ios_enabled = (qrCodeRecord.ios_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id;
ELSE
UPDATE mobile_app_bundle SET ios_app_id = iosAppId WHERE id = generatedBundleId;
END IF;
ELSE
UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.ios_app_id = iosAppId),
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 LOOP;
ALTER TABLE qr_code_settings RENAME CONSTRAINT mobile_app_settings_tenant_id_unq_key TO qr_code_settings_tenant_id_unq_key;
END IF;
ALTER TABLE qr_code_settings DROP COLUMN IF EXISTS android_config, DROP COLUMN IF EXISTS ios_config;
END;
$$;

5
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -74,6 +74,7 @@ import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.mobile.MobileAppBundleService;
import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
@ -385,6 +386,10 @@ public class ActorSystemContext {
@Getter
private MobileAppService mobileAppService;
@Autowired
@Getter
private MobileAppBundleService mobileAppBundleService;
@Autowired
@Getter
private SlackService slackService;

6
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -89,6 +89,7 @@ import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.mobile.MobileAppBundleService;
import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.nosql.CassandraStatementTask;
import org.thingsboard.server.dao.nosql.TbResultSetFuture;
@ -843,6 +844,11 @@ class DefaultTbContext implements TbContext {
return mainCtx.getMobileAppService();
}
@Override
public MobileAppBundleService getMobileAppBundleService() {
return mainCtx.getMobileAppBundleService();
}
@Override
public SlackService getSlackService() {
return mainCtx.getSlackService();

52
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.HomeDashboardInfo;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.StringUtils;
@ -87,6 +88,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.HasId;
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.OtaPackageId;
@ -101,7 +103,8 @@ import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.mobile.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.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
@ -135,6 +138,7 @@ import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.mobile.MobileAppBundleService;
import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.oauth2.OAuth2ClientService;
@ -198,6 +202,11 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@TbCoreComponent
public abstract class BaseController {
protected static final String DASHBOARD_ID = "dashboardId";
protected static final String HOME_DASHBOARD_ID = "homeDashboardId";
protected static final String HOME_DASHBOARD_HIDE_TOOLBAR = "homeDashboardHideToolbar";
protected final Logger log = org.slf4j.LoggerFactory.getLogger(getClass());
/*Swagger UI description*/
@ -262,6 +271,9 @@ public abstract class BaseController {
@Autowired
protected MobileAppService mobileAppService;
@Autowired
protected MobileAppBundleService mobileAppBundleService;
@Autowired
protected OAuth2ConfigTemplateService oAuth2ConfigTemplateService;
@ -645,6 +657,9 @@ public abstract class BaseController {
case MOBILE_APP:
checkMobileAppId(new MobileAppId(entityId.getId()), operation);
return;
case MOBILE_APP_BUNDLE:
checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation);
return;
default:
checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation);
}
@ -833,6 +848,10 @@ public abstract class BaseController {
return checkEntityId(mobileAppId, mobileAppService::findMobileAppById, operation);
}
MobileAppBundle checkMobileAppBundleId(MobileAppBundleId mobileAppBundleId, Operation operation) throws ThingsboardException {
return checkEntityId(mobileAppBundleId, mobileAppBundleService::findMobileAppBundleById, operation);
}
protected <I extends EntityId> I emptyId(EntityType entityType) {
return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
}
@ -915,6 +934,37 @@ public abstract class BaseController {
}
}
protected HomeDashboardInfo getHomeDashboardInfo(SecurityUser securityUser, JsonNode additionalInfo) {
HomeDashboardInfo homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo);
if (homeDashboardInfo == null) {
if (securityUser.isCustomerUser()) {
Customer customer = customerService.findCustomerById(securityUser.getTenantId(), securityUser.getCustomerId());
homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(customer.getAdditionalInfo());
}
if (homeDashboardInfo == null) {
Tenant tenant = tenantService.findTenantById(securityUser.getTenantId());
homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(tenant.getAdditionalInfo());
}
}
return homeDashboardInfo;
}
private HomeDashboardInfo extractHomeDashboardInfoFromAdditionalInfo(JsonNode additionalInfo) {
try {
if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) {
String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText();
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
checkDashboardId(dashboardId, Operation.READ);
boolean hideDashboardToolbar = true;
if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) {
hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean();
}
return new HomeDashboardInfo(dashboardId, hideDashboardToolbar);
}
} catch (Exception ignored) {}
return null;
}
protected MediaType parseMediaType(String contentType) {
try {
return MediaType.parseMediaType(contentType);

36
application/src/main/java/org/thingsboard/server/controller/DashboardController.java

@ -96,10 +96,6 @@ public class DashboardController extends BaseController {
private final TbDashboardService tbDashboardService;
private final ImageService imageService;
public static final String DASHBOARD_ID = "dashboardId";
private static final String HOME_DASHBOARD_ID = "homeDashboardId";
private static final String HOME_DASHBOARD_HIDE_TOOLBAR = "homeDashboardHideToolbar";
public static final String DASHBOARD_INFO_DEFINITION = "The Dashboard Info object contains lightweight information about the dashboard (e.g. title, image, assigned customers) but does not contain the heavyweight configuration JSON.";
public static final String DASHBOARD_DEFINITION = "The Dashboard object is a heavyweight object that contains information about the dashboard (e.g. title, image, assigned customers) and also configuration JSON (e.g. layouts, widgets, entity aliases).";
public static final String HIDDEN_FOR_MOBILE = "Exclude dashboards that are hidden for mobile";
@ -460,21 +456,7 @@ public class DashboardController extends BaseController {
}
User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId());
JsonNode additionalInfo = user.getAdditionalInfo();
HomeDashboardInfo homeDashboardInfo;
homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo);
if (homeDashboardInfo == null) {
if (securityUser.isCustomerUser()) {
Customer customer = customerService.findCustomerById(securityUser.getTenantId(), securityUser.getCustomerId());
additionalInfo = customer.getAdditionalInfo();
homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo);
}
if (homeDashboardInfo == null) {
Tenant tenant = tenantService.findTenantById(securityUser.getTenantId());
additionalInfo = tenant.getAdditionalInfo();
homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo);
}
}
return homeDashboardInfo;
return getHomeDashboardInfo(securityUser, additionalInfo);
}
@ApiOperation(value = "Get Tenant Home Dashboard Info (getTenantHomeDashboardInfo)",
@ -527,22 +509,6 @@ public class DashboardController extends BaseController {
tenantService.saveTenant(tenant);
}
private HomeDashboardInfo extractHomeDashboardInfoFromAdditionalInfo(JsonNode additionalInfo) {
try {
if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) {
String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText();
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
checkDashboardId(dashboardId, Operation.READ);
boolean hideDashboardToolbar = true;
if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) {
hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean();
}
return new HomeDashboardInfo(dashboardId, hideDashboardToolbar);
}
} catch (Exception ignored) {}
return null;
}
private HomeDashboard extractHomeDashboardFromAdditionalInfo(JsonNode additionalInfo) {
try {
if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) {

131
application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java

@ -0,0 +1,131 @@
/**
* 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 io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.mobile.TbMobileAppBundleService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.List;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class MobileAppBundleController extends BaseController {
private final TbMobileAppBundleService tbMobileAppBundleService;
@ApiOperation(value = "Save Or update Mobile app bundle (saveMobileAppBundle)",
notes = "Create or update the Mobile app bundle that represents tha pair of ANDROID and IOS app and " +
"mobile settings like oauth2 clients, self-registration and layout configuration." +
"When creating mobile app bundle, platform generates Mobile App Bundle Id as " + UUID_WIKI_LINK +
"The newly created Mobile App Bundle Id will be present in the response. " +
"Referencing non-existing Mobile App Bundle Id will cause 'Not Found' error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping(value = "/mobile/bundle")
public MobileAppBundle saveMobileAppBundle(
@Parameter(description = "A JSON value representing the Mobile Application Bundle.", required = true)
@RequestBody @Valid MobileAppBundle mobileAppBundle,
@Parameter(description = "A list of oauth2 client ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam(name = "oauth2ClientIds", required = false) UUID[] ids) throws Exception {
mobileAppBundle.setTenantId(getTenantId());
checkEntity(mobileAppBundle.getId(), mobileAppBundle, Resource.MOBILE_APP_BUNDLE);
return tbMobileAppBundleService.save(mobileAppBundle, getOAuth2ClientIds(ids), getCurrentUser());
}
@ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)",
notes = "Update oauth2 clients of the specified mobile app bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PutMapping(value = "/mobile/bundle/{id}/oauth2Clients")
public void updateOauth2Clients(@PathVariable UUID id,
@RequestBody UUID[] clientIds) throws ThingsboardException {
MobileAppBundleId mobileAppBundleId = new MobileAppBundleId(id);
MobileAppBundle mobileAppBundle = checkMobileAppBundleId(mobileAppBundleId, Operation.WRITE);
List<OAuth2ClientId> oAuth2ClientIds = getOAuth2ClientIds(clientIds);
tbMobileAppBundleService.updateOauth2Clients(mobileAppBundle, oAuth2ClientIds, getCurrentUser());
}
@ApiOperation(value = "Get mobile app bundle infos (getTenantMobileAppBundleInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/mobile/bundle/infos")
public PageData<MobileAppBundleInfo> getTenantMobileAppBundleInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = "Case-insensitive 'substring' filter based on app's name")
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return mobileAppBundleService.findMobileAppBundleInfosByTenantId(getTenantId(), pageLink);
}
@ApiOperation(value = "Get mobile app bundle info by id (getMobileAppBundleInfoById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/mobile/bundle/info/{id}")
public MobileAppBundleInfo getMobileAppBundleInfoById(@PathVariable UUID id) throws ThingsboardException {
MobileAppBundleId mobileAppBundleId = new MobileAppBundleId(id);
return checkEntityId(mobileAppBundleId, mobileAppBundleService::findMobileAppBundleInfoById, Operation.READ);
}
@ApiOperation(value = "Delete Mobile App Bundle by ID (deleteMobileAppBundle)",
notes = "Deletes Mobile App Bundle by ID. Referencing non-existing mobile app bundle Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@DeleteMapping(value = "/mobile/bundle/{id}")
public void deleteMobileAppBundle(@PathVariable UUID id) throws Exception {
MobileAppBundleId mobileAppBundleId = new MobileAppBundleId(id);
MobileAppBundle mobileAppBundle = checkMobileAppBundleId(mobileAppBundleId, Operation.DELETE);
tbMobileAppBundleService.delete(mobileAppBundle, getCurrentUser());
}
}

143
application/src/main/java/org/thingsboard/server/controller/MobileAppController.java

@ -15,8 +15,9 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@ -26,33 +27,43 @@ import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.HomeDashboardInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.Views;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileApp;
import org.thingsboard.server.common.data.mobile.MobileAppInfo;
import org.thingsboard.server.common.data.mobile.LoginMobileInfo;
import org.thingsboard.server.common.data.mobile.UserMobileInfo;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.mobile.app.MobileAppVersionInfo;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.layout.MobilePage;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.mobile.TbMobileAppService;
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 java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
@RestController
@ -64,70 +75,104 @@ public class MobileAppController extends BaseController {
private final TbMobileAppService tbMobileAppService;
@ApiOperation(value = "Get mobile app login info (getLoginMobileInfo)")
@GetMapping(value = "/noauth/mobile")
public LoginMobileInfo getLoginMobileInfo(@Parameter(description = "Mobile application package name")
@RequestParam String pkgName,
@Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"}))
@RequestParam PlatformType platform) {
List<OAuth2ClientLoginInfo> oauth2Clients = oAuth2ClientService.findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(pkgName, platform);
MobileApp mobileApp = mobileAppService.findMobileAppByPkgNameAndPlatformType(pkgName, platform);
return new LoginMobileInfo(oauth2Clients, mobileApp != null ? mobileApp.getVersionInfo() : null);
}
@ApiOperation(value = "Get user mobile app basic info (getUserMobileInfo)", notes = AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/mobile")
public UserMobileInfo getUserMobileInfo(@Parameter(description = "Mobile application package name")
@RequestParam String pkgName,
@Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"}))
@RequestParam PlatformType platform) throws ThingsboardException, JsonProcessingException {
SecurityUser securityUser = getCurrentUser();
User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId());
HomeDashboardInfo homeDashboardInfo = securityUser.isSystemAdmin() ? null : getHomeDashboardInfo(securityUser, user.getAdditionalInfo());
MobileAppBundle mobileAppBundle = mobileAppBundleService.findMobileAppBundleByPkgNameAndPlatform(securityUser.getTenantId(), pkgName, platform);
return new UserMobileInfo(user, homeDashboardInfo, getVisiblePages(mobileAppBundle));
}
@ApiOperation(value = "Get mobile app version info (getMobileVersionInfo)")
@GetMapping(value = "/mobile/versionInfo")
public MobileAppVersionInfo getMobileVersionInfo(@Parameter(description = "Mobile application package name")
@RequestParam String pkgName,
@Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"}))
@RequestParam PlatformType platform) {
MobileApp mobileApp = mobileAppService.findMobileAppByPkgNameAndPlatformType(pkgName, platform);
return mobileApp != null ? mobileApp.getVersionInfo() : null;
}
@ApiOperation(value = "Save Or update Mobile app (saveMobileApp)",
notes = "Create or update the Mobile app. When creating mobile app, platform generates Mobile App Id as " + UUID_WIKI_LINK +
"The newly created Mobile App Id will be present in the response. " +
"Specify existing Mobile App Id to update the mobile app. " +
"Referencing non-existing Mobile App Id will cause 'Not Found' error." +
"\n\nMobile app package name is unique for entire platform setup.\n\n" + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@PostMapping(value = "/mobileApp")
"\n\nThe pair of mobile app package name and platform type is unique for entire platform setup.\n\n" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping(value = "/mobile/app")
public MobileApp saveMobileApp(
@Parameter(description = "A JSON value representing the Mobile Application.", required = true)
@RequestBody @Valid MobileApp mobileApp,
@Parameter(description = "A list of entity oauth2 client ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam(name = "oauth2ClientIds", required = false) UUID[] ids) throws Exception {
@RequestBody @Valid MobileApp mobileApp) throws Exception {
mobileApp.setTenantId(getTenantId());
checkEntity(mobileApp.getId(), mobileApp, Resource.MOBILE_APP);
return tbMobileAppService.save(mobileApp, getOAuth2ClientIds(ids), getCurrentUser());
return tbMobileAppService.save(mobileApp, getCurrentUser());
}
@ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)",
notes = "Update oauth2 clients of the specified mobile app. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@PutMapping(value = "/mobileApp/{id}/oauth2Clients")
public void updateOauth2Clients(@PathVariable UUID id,
@RequestBody UUID[] clientIds) throws ThingsboardException {
MobileAppId mobileAppId = new MobileAppId(id);
MobileApp mobileApp = checkMobileAppId(mobileAppId, Operation.WRITE);
List<OAuth2ClientId> oAuth2ClientIds = getOAuth2ClientIds(clientIds);
tbMobileAppService.updateOauth2Clients(mobileApp, oAuth2ClientIds, getCurrentUser());
}
@ApiOperation(value = "Get mobile app infos (getTenantMobileAppInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@GetMapping(value = "/mobileApp/infos")
public PageData<MobileAppInfo> getTenantMobileAppInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = "Case-insensitive 'substring' filter based on app's name")
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.MOBILE_APP, Operation.READ);
@ApiOperation(value = "Get mobile app infos (getTenantMobileAppInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/mobile/app")
public PageData<MobileApp> getTenantMobileApps(@Parameter(description = "Platform type: ANDROID or IOS")
@RequestParam(required = false) PlatformType platformType,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = "Case-insensitive 'substring' filter based on app's name")
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return mobileAppService.findMobileAppInfosByTenantId(getTenantId(), pageLink);
return mobileAppService.findMobileAppsByTenantId(getTenantId(), platformType, pageLink);
}
@ApiOperation(value = "Get mobile info by id (getMobileAppInfoById)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@GetMapping(value = "/mobileApp/info/{id}")
public MobileAppInfo getMobileAppInfoById(@PathVariable UUID id) throws ThingsboardException {
@ApiOperation(value = "Get mobile info by id (getMobileAppInfoById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/mobile/app/{id}")
public MobileApp getMobileAppById(@PathVariable UUID id) throws ThingsboardException {
MobileAppId mobileAppId = new MobileAppId(id);
return checkEntityId(mobileAppId, mobileAppService::findMobileAppInfoById, Operation.READ);
return checkEntityId(mobileAppId, mobileAppService::findMobileAppById, Operation.READ);
}
@ApiOperation(value = "Delete Mobile App by ID (deleteMobileApp)",
notes = "Deletes Mobile App by ID. Referencing non-existing mobile app Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@DeleteMapping(value = "/mobileApp/{id}")
notes = "Deletes Mobile App by ID. Referencing non-existing mobile app Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@DeleteMapping(value = "/mobile/app/{id}")
public void deleteMobileApp(@PathVariable UUID id) throws Exception {
MobileAppId mobileAppId = new MobileAppId(id);
MobileApp mobileApp = checkMobileAppId(mobileAppId, Operation.DELETE);
tbMobileAppService.delete(mobileApp, getCurrentUser());
}
private JsonNode getVisiblePages(MobileAppBundle mobileAppBundle) throws JsonProcessingException {
if (mobileAppBundle != null && mobileAppBundle.getLayoutConfig() != null) {
List<MobilePage> mobilePages = mobileAppBundle.getLayoutConfig().getPages()
.stream()
.filter(MobilePage::isVisible)
.collect(Collectors.toList());
return JacksonUtil.toJsonNode(JacksonUtil.writeValueAsViewIgnoringNullFields(mobilePages, Views.Public.class));
} else {
return JacksonUtil.newArrayNode();
}
}
}

29
application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java

@ -59,6 +59,7 @@ import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DE
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent
@ -104,8 +105,8 @@ public class OAuth2Controller extends BaseController {
}
}
@ApiOperation(value = "Save OAuth2 Client (saveOAuth2Client)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@ApiOperation(value = "Save OAuth2 Client (saveOAuth2Client)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping(value = "/oauth2/client")
public OAuth2Client saveOAuth2Client(@RequestBody @Valid OAuth2Client oAuth2Client) throws Exception {
TenantId tenantId = getTenantId();
@ -114,8 +115,8 @@ public class OAuth2Controller extends BaseController {
return tbOauth2ClientService.save(oAuth2Client, getCurrentUser());
}
@ApiOperation(value = "Get OAuth2 Client infos (findTenantOAuth2ClientInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@ApiOperation(value = "Get OAuth2 Client infos (findTenantOAuth2ClientInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/oauth2/client/infos")
public PageData<OAuth2ClientInfo> findTenantOAuth2ClientInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -127,14 +128,13 @@ public class OAuth2Controller extends BaseController {
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CLIENT, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return oAuth2ClientService.findOAuth2ClientInfosByTenantId(getTenantId(), pageLink);
}
@ApiOperation(value = "Get OAuth2 Client infos By Ids (findTenantOAuth2ClientInfosByIds)",
notes = "Fetch OAuth2 Client info objects based on the provided ids. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
notes = "Fetch OAuth2 Client info objects based on the provided ids. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/oauth2/client/infos", params = {"clientIds"})
public List<OAuth2ClientInfo> findTenantOAuth2ClientInfosByIds(
@Parameter(description = "A list of oauth2 ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@ -143,8 +143,8 @@ public class OAuth2Controller extends BaseController {
return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds);
}
@ApiOperation(value = "Get OAuth2 Client by id (getOAuth2ClientById)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@ApiOperation(value = "Get OAuth2 Client by id (getOAuth2ClientById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/oauth2/client/{id}")
public OAuth2Client getOAuth2ClientById(@PathVariable UUID id) throws ThingsboardException {
OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(id);
@ -152,8 +152,8 @@ public class OAuth2Controller extends BaseController {
}
@ApiOperation(value = "Delete oauth2 client (deleteOauth2Client)",
notes = "Deletes the oauth2 client. Referencing non-existing oauth2 client Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
notes = "Deletes the oauth2 client. Referencing non-existing oauth2 client Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@DeleteMapping(value = "/oauth2/client/{id}")
public void deleteOauth2Client(@PathVariable UUID id) throws Exception {
OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(id);
@ -164,11 +164,10 @@ public class OAuth2Controller extends BaseController {
@ApiOperation(value = "Get OAuth2 log in processing URL (getLoginProcessingUrl)", notes = "Returns the URL enclosed in " +
"double quotes. After successful authentication with OAuth2 provider, it makes a redirect to this path so that the platform can do " +
"further log in processing. This URL may be configured as 'security.oauth2.loginProcessingUrl' property in yml configuration file, or " +
"as 'SECURITY_OAUTH2_LOGIN_PROCESSING_URL' env variable. By default it is '/login/oauth2/code/'" + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
"as 'SECURITY_OAUTH2_LOGIN_PROCESSING_URL' env variable. By default it is '/login/oauth2/code/'" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/oauth2/loginProcessingUrl")
public String getLoginProcessingUrl() throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CLIENT, Operation.READ);
public String getLoginProcessingUrl() {
return "\"" + oAuth2Configuration.getLoginProcessingUrl() + "\"";
}

71
application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java → application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java

@ -31,13 +31,16 @@ 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.MobileAppBundleId;
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.app.MobileApp;
import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings;
import org.thingsboard.server.common.data.mobile.app.StoreInfo;
import org.thingsboard.server.common.data.oauth2.PlatformType;
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.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.mobile.QrCodeSettingService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.mobile.secret.MobileAppSecretService;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -48,13 +51,15 @@ import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.net.URI;
import java.net.URISyntaxException;
import static org.thingsboard.server.common.data.oauth2.PlatformType.ANDROID;
import static org.thingsboard.server.common.data.oauth2.PlatformType.IOS;
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 {
public class QrCodeSettingsController extends BaseController {
@Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}")
private int mobileSecretKeyTtl;
@ -89,15 +94,15 @@ public class MobileApplicationController extends BaseController {
private final SystemSecurityService systemSecurityService;
private final MobileAppSecretService mobileAppSecretService;
private final MobileAppSettingsService mobileAppSettingsService;
private final QrCodeSettingService qrCodeSettingService;
@ApiOperation(value = "Get associated android applications (getAssetLinks)")
@GetMapping(value = "/.well-known/assetlinks.json")
public ResponseEntity<JsonNode> getAssetLinks() {
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
AndroidConfig androidConfig = mobileAppSettings.getAndroidConfig();
if (androidConfig != null && androidConfig.isEnabled() && androidConfig.getAppPackage() != null && androidConfig.getSha256CertFingerprints() != null) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, androidConfig.getAppPackage(), androidConfig.getSha256CertFingerprints())));
MobileApp mobileApp = qrCodeSettingService.findAppFromQrCodeSettings(TenantId.SYS_TENANT_ID, ANDROID);
StoreInfo storeInfo = mobileApp != null ? mobileApp.getStoreInfo() : null;
if (storeInfo != null && storeInfo.getSha256CertFingerprints() != null) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, mobileApp.getPkgName(), storeInfo.getSha256CertFingerprints())));
} else {
return ResponseEntity.notFound().build();
}
@ -106,10 +111,10 @@ public class MobileApplicationController extends BaseController {
@ApiOperation(value = "Get associated ios applications (getAppleAppSiteAssociation)")
@GetMapping(value = "/.well-known/apple-app-site-association")
public ResponseEntity<JsonNode> getAppleAppSiteAssociation() {
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
IosConfig iosConfig = mobileAppSettings.getIosConfig();
if (iosConfig != null && iosConfig.isEnabled() && iosConfig.getAppId() != null) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, iosConfig.getAppId())));
MobileApp mobileApp = qrCodeSettingService.findAppFromQrCodeSettings(TenantId.SYS_TENANT_ID, IOS);
StoreInfo storeInfo = mobileApp != null ? mobileApp.getStoreInfo() : null;
if (storeInfo != null && storeInfo.getAppId() != null) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, storeInfo.getAppId())));
} else {
return ResponseEntity.notFound().build();
}
@ -118,40 +123,35 @@ public class MobileApplicationController extends BaseController {
@ApiOperation(value = "Create Or Update the Mobile application settings (saveMobileAppSettings)",
notes = "The request payload contains configuration for android/iOS applications and platform qr code widget settings." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@PostMapping(value = "/api/mobile/app/settings")
public MobileAppSettings saveMobileAppSettings(@Parameter(description = "A JSON value representing the mobile apps configuration")
@RequestBody MobileAppSettings mobileAppSettings) throws ThingsboardException {
@PostMapping(value = "/api/mobile/qr/settings")
public QrCodeSettings saveQrCodeSettings(@Parameter(description = "A JSON value representing the mobile apps configuration")
@RequestBody QrCodeSettings qrCodeSettings) throws ThingsboardException {
SecurityUser currentUser = getCurrentUser();
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.WRITE);
mobileAppSettings.setTenantId(getTenantId());
return mobileAppSettingsService.saveMobileAppSettings(currentUser.getTenantId(), mobileAppSettings);
qrCodeSettings.setTenantId(getTenantId());
return qrCodeSettingService.saveQrCodeSettings(currentUser.getTenantId(), qrCodeSettings);
}
@ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)",
notes = "The response payload contains configuration for android/iOS applications and platform qr code widget settings." + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/api/mobile/app/settings")
public MobileAppSettings getMobileAppSettings() throws ThingsboardException {
@GetMapping(value = "/api/mobile/qr/settings")
public QrCodeSettings getQrCodeSettings() throws ThingsboardException {
SecurityUser currentUser = getCurrentUser();
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ);
return mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
return qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID);
}
@ApiOperation(value = "Get the deep link to the associated mobile application (getMobileAppDeepLink)",
notes = "Fetch the url that takes user to linked mobile application " + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/api/mobile/deepLink", produces = "text/plain")
@GetMapping(value = "/api/mobile/qr/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, null, request);
String platformDomain = new URI(baseUrl).getHost();
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
String appDomain;
if (!mobileAppSettings.isUseDefaultApp()) {
appDomain = platformDomain;
} else {
appDomain = defaultAppDomain;
}
QrCodeSettings qrCodeSettings = qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID);
String appDomain = qrCodeSettings.isUseDefaultApp() ? defaultAppDomain : platformDomain;
String deepLink = String.format(DEEP_LINK_PATTERN, appDomain, secret, mobileSecretKeyTtl);
if (!appDomain.equals(platformDomain)) {
deepLink = deepLink + "&host=" + baseUrl;
@ -170,15 +170,14 @@ public class MobileApplicationController extends BaseController {
@GetMapping(value = "/api/noauth/qr")
public ResponseEntity<?> getApplicationRedirect(@RequestHeader(value = "User-Agent") String userAgent) {
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
boolean useDefaultApp = mobileAppSettings.isUseDefaultApp();
String googlePlayLink = useDefaultApp ? mobileAppSettings.getDefaultGooglePlayLink() : mobileAppSettings.getAndroidConfig().getStoreLink();
String appStoreLink = useDefaultApp ? mobileAppSettings.getDefaultAppStoreLink() : mobileAppSettings.getIosConfig().getStoreLink();
if (userAgent.contains("Android")) {
QrCodeSettings qrCodeSettings = qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID);
if (userAgent.contains("Android") && qrCodeSettings.isAndroidEnabled()) {
String googlePlayLink = qrCodeSettings.getGooglePlayLink();
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", googlePlayLink)
.build();
} else if (userAgent.contains("iPhone") || userAgent.contains("iPad")) {
} else if (userAgent.contains("iPhone") || userAgent.contains("iPad") && qrCodeSettings.isIosEnabled()) {
String appStoreLink = qrCodeSettings.getAppStoreLink();
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", appStoreLink)
.build();

12
application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java

@ -34,13 +34,13 @@ import org.thingsboard.server.common.data.SystemParams;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.mobile.QRCodeConfig;
import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings;
import org.thingsboard.server.common.data.mobile.qrCodeSettings.QRCodeConfig;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.mobile.MobileAppSettingsService;
import org.thingsboard.server.dao.mobile.QrCodeSettingService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
@ -77,7 +77,7 @@ public class SystemInfoController extends BaseController {
private EntitiesVersionControlService versionControlService;
@Autowired
private MobileAppSettingsService mobileAppSettingsService;
private QrCodeSettingService qrCodeSettingService;
@PostConstruct
public void init() {
@ -142,8 +142,8 @@ public class SystemInfoController extends BaseController {
DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration();
systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize());
}
systemParams.setMobileQrEnabled(Optional.ofNullable(mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID))
.map(MobileAppSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage)
systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID))
.map(QrCodeSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage)
.orElse(false));
return systemParams;
}

83
application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppBundleService.java

@ -0,0 +1,83 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.entitiy.mobile;
import lombok.AllArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import org.thingsboard.server.dao.mobile.MobileAppBundleService;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import java.util.List;
@Service
@AllArgsConstructor
public class DefaultTbMobileAppBundleService extends AbstractTbEntityService implements TbMobileAppBundleService {
private final MobileAppBundleService mobileAppBundleService;
@Override
public MobileAppBundle save(MobileAppBundle mobileAppBundle, List<OAuth2ClientId> oauth2Clients, User user) throws Exception {
ActionType actionType = mobileAppBundle.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = mobileAppBundle.getTenantId();
try {
MobileAppBundle savedMobileAppBundle = checkNotNull(mobileAppBundleService.saveMobileAppBundle(tenantId, mobileAppBundle));
if (CollectionUtils.isNotEmpty(oauth2Clients)) {
mobileAppBundleService.updateOauth2Clients(tenantId, savedMobileAppBundle.getId(), oauth2Clients);
}
logEntityActionService.logEntityAction(tenantId, savedMobileAppBundle.getId(), savedMobileAppBundle, actionType, user);
return savedMobileAppBundle;
} catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.MOBILE_APP), mobileAppBundle, actionType, user, e);
throw e;
}
}
@Override
public void updateOauth2Clients(MobileAppBundle mobileAppBundle, List<OAuth2ClientId> oAuth2ClientIds, User user) {
ActionType actionType = ActionType.UPDATED;
TenantId tenantId = mobileAppBundle.getTenantId();
MobileAppBundleId mobileAppBundleId = mobileAppBundle.getId();
try {
mobileAppBundleService.updateOauth2Clients(tenantId, mobileAppBundleId, oAuth2ClientIds);
logEntityActionService.logEntityAction(tenantId, mobileAppBundleId, mobileAppBundle, actionType, user, oAuth2ClientIds);
} catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, mobileAppBundleId, mobileAppBundle, actionType, user, e, oAuth2ClientIds);
throw e;
}
}
@Override
public void delete(MobileAppBundle mobileAppBundle, User user) {
ActionType actionType = ActionType.DELETED;
TenantId tenantId = mobileAppBundle.getTenantId();
MobileAppBundleId mobileAppBundleId = mobileAppBundle.getId();
try {
mobileAppBundleService.deleteMobileAppBundleById(tenantId, mobileAppBundleId);
logEntityActionService.logEntityAction(tenantId, mobileAppBundleId, mobileAppBundle, actionType, user);
} catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, mobileAppBundleId, mobileAppBundle, actionType, user, e);
throw e;
}
}
}

24
application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java

@ -16,20 +16,16 @@
package org.thingsboard.server.service.entitiy.mobile;
import lombok.AllArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileApp;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import java.util.List;
@Service
@AllArgsConstructor
public class DefaultTbMobileAppService extends AbstractTbEntityService implements TbMobileAppService {
@ -37,14 +33,11 @@ public class DefaultTbMobileAppService extends AbstractTbEntityService implement
private final MobileAppService mobileAppService;
@Override
public MobileApp save(MobileApp mobileApp, List<OAuth2ClientId> oauth2Clients, User user) throws Exception {
public MobileApp save(MobileApp mobileApp, User user) throws Exception {
ActionType actionType = mobileApp.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = mobileApp.getTenantId();
try {
MobileApp savedMobileApp = checkNotNull(mobileAppService.saveMobileApp(tenantId, mobileApp));
if (CollectionUtils.isNotEmpty(oauth2Clients)) {
mobileAppService.updateOauth2Clients(tenantId, savedMobileApp.getId(), oauth2Clients);
}
logEntityActionService.logEntityAction(tenantId, savedMobileApp.getId(), savedMobileApp, actionType, user);
return savedMobileApp;
} catch (Exception e) {
@ -53,19 +46,6 @@ public class DefaultTbMobileAppService extends AbstractTbEntityService implement
}
}
@Override
public void updateOauth2Clients(MobileApp mobileApp, List<OAuth2ClientId> oAuth2ClientIds, User user) {
ActionType actionType = ActionType.UPDATED;
TenantId tenantId = mobileApp.getTenantId();
MobileAppId mobileAppId = mobileApp.getId();
try {
mobileAppService.updateOauth2Clients(tenantId, mobileAppId, oAuth2ClientIds);
logEntityActionService.logEntityAction(tenantId, mobileAppId, mobileApp, actionType, user, oAuth2ClientIds);
} catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, mobileAppId, mobileApp, actionType, user, e, oAuth2ClientIds);
throw e;
}
}
@Override
public void delete(MobileApp mobileApp, User user) {

32
application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppBundleService.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.entitiy.mobile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import java.util.List;
public interface TbMobileAppBundleService {
MobileAppBundle save(MobileAppBundle mobileAppBundle, List<OAuth2ClientId> oauth2Clients, User user) throws Exception;
void updateOauth2Clients(MobileAppBundle mobileAppBundle, List<OAuth2ClientId> oAuth2ClientIds, User user);
void delete(MobileAppBundle mobileAppBundle, User user);
}

9
application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java

@ -16,16 +16,11 @@
package org.thingsboard.server.service.entitiy.mobile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.mobile.MobileApp;
import java.util.List;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
public interface TbMobileAppService {
MobileApp save(MobileApp mobileApp, List<OAuth2ClientId> oauth2Clients, User user) throws Exception;
void updateOauth2Clients(MobileApp mobileApp, List<OAuth2ClientId> oAuth2ClientIds, User user);
MobileApp save(MobileApp mobileApp, User user) throws Exception;
void delete(MobileApp mobileApp, User user);

4
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -69,7 +69,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.mobile.MobileApp;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
@ -308,7 +308,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
jwtSettingsService.saveJwtSettings(jwtSettings);
}
List<MobileApp> mobiles = mobileAppDao.findByTenantId(TenantId.SYS_TENANT_ID, new PageLink(Integer.MAX_VALUE,0)).getData();
List<MobileApp> mobiles = mobileAppDao.findByTenantId(TenantId.SYS_TENANT_ID, null, new PageLink(Integer.MAX_VALUE,0)).getData();
if (CollectionUtils.isNotEmpty(mobiles)) {
mobiles.stream()
.filter(mobileApp -> !validateKeyLength(mobileApp.getAppSecret()))

1
application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java

@ -36,6 +36,7 @@ public enum Resource {
OAUTH2_CLIENT(EntityType.OAUTH2_CLIENT),
DOMAIN(EntityType.DOMAIN),
MOBILE_APP(EntityType.MOBILE_APP),
MOBILE_APP_BUNDLE(EntityType.MOBILE_APP_BUNDLE),
OAUTH2_CONFIGURATION_TEMPLATE(),
TENANT_PROFILE(EntityType.TENANT_PROFILE),
DEVICE_PROFILE(EntityType.DEVICE_PROFILE),

5
application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java

@ -35,8 +35,9 @@ public class SysAdminPermissions extends AbstractPermissions {
put(Resource.USER, userPermissionChecker);
put(Resource.WIDGETS_BUNDLE, systemEntityPermissionChecker);
put(Resource.WIDGET_TYPE, systemEntityPermissionChecker);
put(Resource.OAUTH2_CLIENT, PermissionChecker.allowAllPermissionChecker);
put(Resource.MOBILE_APP, PermissionChecker.allowAllPermissionChecker);
put(Resource.OAUTH2_CLIENT, systemEntityPermissionChecker);
put(Resource.MOBILE_APP, systemEntityPermissionChecker);
put(Resource.MOBILE_APP_BUNDLE, systemEntityPermissionChecker);
put(Resource.DOMAIN, PermissionChecker.allowAllPermissionChecker);
put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, PermissionChecker.allowAllPermissionChecker);
put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker);

4
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -51,6 +51,10 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker);
put(Resource.NOTIFICATION, tenantEntityPermissionChecker);
put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ));
put(Resource.OAUTH2_CLIENT, tenantEntityPermissionChecker);
put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, new PermissionChecker.GenericPermissionChecker(Operation.READ));
put(Resource.MOBILE_APP, tenantEntityPermissionChecker);
put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker);
}
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {

4
application/src/main/resources/thingsboard.yml

@ -609,8 +609,8 @@ cache:
alarmTypes:
timeToLiveInMinutes: "${CACHE_SPECS_ALARM_TYPES_TTL:60}" # Alarm types cache TTL
maxSize: "${CACHE_SPECS_ALARM_TYPES_MAX_SIZE:10000}" # 0 means the cache is disabled
mobileAppSettings:
timeToLiveInMinutes: "${CACHE_SPECS_MOBILE_APP_SETTINGS_TTL:1440}" # Mobile application cache TTL
qrCodeSettings:
timeToLiveInMinutes: "${CACHE_SPECS_MOBILE_APP_SETTINGS_TTL:1440}" # Qr code settings cache TTL
maxSize: "${CACHE_SPECS_MOBILE_APP_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled
mobileSecretKey:
timeToLiveInMinutes: "${CACHE_MOBILE_SECRET_KEY_TTL:2}" # QR secret key cache TTL

169
application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java

@ -0,0 +1,169 @@
/**
* 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.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.TenantId;
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.mobile.bundle.MobileAppBundleInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
@DaoSqlTest
public class MobileAppBundleControllerTest extends AbstractControllerTest {
static final TypeReference<PageData<MobileAppBundleInfo>> PAGE_DATA_MOBILE_APP_BUNDLE_TYPE_REF = new TypeReference<>() {
};
static final TypeReference<PageData<MobileApp>> PAGE_DATA_MOBILE_APP_TYPE_REF = new TypeReference<>() {
};
static final TypeReference<PageData<OAuth2ClientInfo>> PAGE_DATA_OAUTH2_CLIENT_TYPE_REF = new TypeReference<>() {
};
private MobileApp androidApp;
private MobileApp iosApp;
@Before
public void setUp() throws Exception {
loginSysAdmin();
androidApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.android.package", PlatformType.ANDROID);
androidApp = doPost("/api/mobile/app", androidApp, MobileApp.class);
iosApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.ios.package", PlatformType.IOS);
iosApp = doPost("/api/mobile/app", iosApp, MobileApp.class);
}
@After
public void tearDown() throws Exception {
PageData<MobileAppBundleInfo> pageData2 = doGetTypedWithPageLink("/api/mobile/bundle/infos?", PAGE_DATA_MOBILE_APP_BUNDLE_TYPE_REF, new PageLink(10, 0));
for (MobileAppBundleInfo appBundleInfo : pageData2.getData()) {
doDelete("/api/mobile/bundle/" + appBundleInfo.getId().getId())
.andExpect(status().isOk());
}
PageData<MobileApp> pageData = doGetTypedWithPageLink("/api/mobile/app?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0));
for (MobileApp mobileApp : pageData.getData()) {
doDelete("/api/mobile/app/" + mobileApp.getId().getId())
.andExpect(status().isOk());
}
PageData<OAuth2ClientInfo> clients = doGetTypedWithPageLink("/api/oauth2/client/infos?", PAGE_DATA_OAUTH2_CLIENT_TYPE_REF, new PageLink(10, 0));
for (OAuth2ClientInfo oAuth2ClientInfo : clients.getData()) {
doDelete("/api/oauth2/client/" + oAuth2ClientInfo.getId().getId().toString())
.andExpect(status().isOk());
}
}
@Test
public void testSaveMobileAppBundle() {
MobileAppBundle mobileAppBundle = new MobileAppBundle();
mobileAppBundle.setTitle("Test bundle");
mobileAppBundle.setAndroidAppId(androidApp.getId());
mobileAppBundle.setIosAppId(iosApp.getId());
MobileAppBundle createdMobileAppBundle = doPost("/api/mobile/bundle", mobileAppBundle, MobileAppBundle.class);
assertThat(createdMobileAppBundle.getAndroidAppId()).isEqualTo(androidApp.getId());
assertThat(createdMobileAppBundle.getIosAppId()).isEqualTo(iosApp.getId());
}
@Test
public void testSaveMobileAppBundleWithoutApps() throws Exception {
MobileAppBundle mobileAppBundle = new MobileAppBundle();
mobileAppBundle.setTitle("Test bundle");
MobileAppBundle savedAppBundle = doPost("/api/mobile/bundle", mobileAppBundle, MobileAppBundle.class);
MobileAppBundleInfo retrievedMobileAppBundleInfo = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedAppBundle.getId().getId());
assertThat(retrievedMobileAppBundleInfo).isEqualTo(new MobileAppBundleInfo(savedAppBundle, null, null, false,
Collections.emptyList()));
}
@Test
public void testUpdateMobileAppBundleOauth2Clients() throws Exception {
MobileAppBundle mobileAppBundle = new MobileAppBundle();
mobileAppBundle.setTitle("Test bundle");
mobileAppBundle.setAndroidAppId(androidApp.getId());
mobileAppBundle.setIosAppId(iosApp.getId());
MobileAppBundle savedAppBundle = doPost("/api/mobile/bundle", mobileAppBundle, MobileAppBundle.class);
OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client");
OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class);
OAuth2Client oAuth2Client2 = createOauth2Client(TenantId.SYS_TENANT_ID, "test facebook client");
OAuth2Client savedOAuth2Client2 = doPost("/api/oauth2/client", oAuth2Client2, OAuth2Client.class);
doPut("/api/mobile/bundle/" + savedAppBundle.getId() + "/oauth2Clients", List.of(savedOAuth2Client.getId().getId(), savedOAuth2Client2.getId().getId()));
MobileAppBundleInfo retrievedMobileAppBundleInfo = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedAppBundle.getId().getId());
assertThat(retrievedMobileAppBundleInfo).isEqualTo(new MobileAppBundleInfo(savedAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), false,
Stream.of(new OAuth2ClientInfo(savedOAuth2Client), new OAuth2ClientInfo(savedOAuth2Client2))
.sorted(Comparator.comparing(OAuth2ClientInfo::getTitle)).collect(Collectors.toList())
));
doPut("/api/mobile/bundle/" + savedAppBundle.getId() + "/oauth2Clients", List.of(savedOAuth2Client2.getId().getId()));
MobileAppBundleInfo retrievedMobileAppInfo2 = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedAppBundle.getId().getId());
assertThat(retrievedMobileAppInfo2).isEqualTo(new MobileAppBundleInfo(savedAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), false, List.of(new OAuth2ClientInfo(savedOAuth2Client2))));
}
@Test
public void testCreateMobileAppBundleWithOauth2Clients() throws Exception {
OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client");
OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class);
MobileAppBundle mobileAppBundle = new MobileAppBundle();
mobileAppBundle.setTitle("Test bundle");
mobileAppBundle.setAndroidAppId(androidApp.getId());
mobileAppBundle.setIosAppId(iosApp.getId());
MobileAppBundle savedMobileAppBundle = doPost("/api/mobile/bundle?oauth2ClientIds=" + savedOAuth2Client.getId().getId(), mobileAppBundle, MobileAppBundle.class);
MobileAppBundleInfo retrievedMobileAppInfo = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedMobileAppBundle.getId().getId());
assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppBundleInfo(savedMobileAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), false, List.of(new OAuth2ClientInfo(savedOAuth2Client))));
}
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;
}
}

104
application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java

@ -21,18 +21,13 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileApp;
import org.thingsboard.server.common.data.mobile.MobileAppInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.mobile.app.MobileAppStatus;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -41,9 +36,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@DaoSqlTest
public class MobileAppControllerTest extends AbstractControllerTest {
static final TypeReference<PageData<MobileAppInfo>> PAGE_DATA_MOBILE_APP_TYPE_REF = new TypeReference<>() {
};
static final TypeReference<PageData<OAuth2ClientInfo>> PAGE_DATA_OAUTH2_CLIENT_TYPE_REF = new TypeReference<>() {
static final TypeReference<PageData<MobileApp>> PAGE_DATA_MOBILE_APP_TYPE_REF = new TypeReference<>() {
};
@Before
@ -53,89 +46,64 @@ public class MobileAppControllerTest extends AbstractControllerTest {
@After
public void tearDown() throws Exception {
PageData<MobileAppInfo> pageData = doGetTypedWithPageLink("/api/mobileApp/infos?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0));
PageData<MobileApp> pageData = doGetTypedWithPageLink("/api/mobile/app?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0));
for (MobileApp mobileApp : pageData.getData()) {
doDelete("/api/mobileApp/" + mobileApp.getId().getId())
.andExpect(status().isOk());
}
PageData<OAuth2ClientInfo> clients = doGetTypedWithPageLink("/api/oauth2/client/infos?", PAGE_DATA_OAUTH2_CLIENT_TYPE_REF, new PageLink(10, 0));
for (OAuth2ClientInfo oAuth2ClientInfo : clients.getData()) {
doDelete("/api/oauth2/client/" + oAuth2ClientInfo.getId().getId().toString())
doDelete("/api/mobile/app/" + mobileApp.getId().getId())
.andExpect(status().isOk());
}
}
@Test
public void testSaveMobileApp() throws Exception {
PageData<MobileAppInfo> pageData = doGetTypedWithPageLink("/api/mobileApp/infos?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0));
PageData<MobileApp> pageData = doGetTypedWithPageLink("/api/mobile/app?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0));
assertThat(pageData.getData()).isEmpty();
MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.test.package", true);
MobileApp savedMobileApp = doPost("/api/mobileApp", mobileApp, MobileApp.class);
MobileApp mobileApp = validMobileApp("my.test.package", PlatformType.ANDROID);
MobileApp savedMobileApp = doPost("/api/mobile/app", mobileApp, MobileApp.class);
PageData<MobileAppInfo> pageData2 = doGetTypedWithPageLink("/api/mobileApp/infos?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0));
PageData<MobileApp> pageData2 = doGetTypedWithPageLink("/api/mobile/app?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0));
assertThat(pageData2.getData()).hasSize(1);
assertThat(pageData2.getData().get(0)).isEqualTo(new MobileAppInfo(savedMobileApp, Collections.emptyList()));
assertThat(pageData2.getData().get(0)).isEqualTo(savedMobileApp);
MobileAppInfo retrievedMobileAppInfo = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId());
assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppInfo(savedMobileApp, Collections.emptyList()));
MobileApp retrievedMobileAppInfo = doGet("/api/mobile/app/{id}", MobileApp.class, savedMobileApp.getId().getId());
assertThat(retrievedMobileAppInfo).isEqualTo(savedMobileApp);
doDelete("/api/mobileApp/" + savedMobileApp.getId().getId());
doGet("/api/mobileApp/info/{id}", savedMobileApp.getId().getId())
doDelete("/api/mobile/app/" + savedMobileApp.getId().getId());
doGet("/api/mobile/app/{id}", savedMobileApp.getId().getId())
.andExpect(status().isNotFound());
}
@Test
public void testSaveMobileAppWithShortAppSecret() throws Exception {
MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "mobileApp.ce", true);
MobileApp mobileApp = validMobileApp( "mobileApp.ce", PlatformType.ANDROID);
mobileApp.setAppSecret("short");
doPost("/api/mobileApp", mobileApp)
doPost("/api/mobile/app", mobileApp)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("appSecret must be at least 16 and max 2048 characters")));
}
@Test
public void testUpdateMobileAppOauth2Clients() throws Exception {
MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.test.package", true);
MobileApp savedMobileApp = doPost("/api/mobileApp", mobileApp, MobileApp.class);
OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client");
OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class);
OAuth2Client oAuth2Client2 = createOauth2Client(TenantId.SYS_TENANT_ID, "test facebook client");
OAuth2Client savedOAuth2Client2 = doPost("/api/oauth2/client", oAuth2Client2, OAuth2Client.class);
doPut("/api/mobileApp/" + savedMobileApp.getId() + "/oauth2Clients", List.of(savedOAuth2Client.getId().getId(), savedOAuth2Client2.getId().getId()));
MobileAppInfo retrievedMobileAppInfo = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId());
assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppInfo(savedMobileApp, List.of(new OAuth2ClientInfo(savedOAuth2Client2),
new OAuth2ClientInfo(savedOAuth2Client))));
doPut("/api/mobileApp/" + savedMobileApp.getId() + "/oauth2Clients", List.of(savedOAuth2Client2.getId().getId()));
MobileAppInfo retrievedMobileAppInfo2 = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId());
assertThat(retrievedMobileAppInfo2).isEqualTo(new MobileAppInfo(savedMobileApp, List.of(new OAuth2ClientInfo(savedOAuth2Client2))));
}
@Test
public void testCreateMobileAppWithOauth2Clients() throws Exception {
OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client");
OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class);
MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.test.package", true);
MobileApp savedMobileApp = doPost("/api/mobileApp?oauth2ClientIds=" + savedOAuth2Client.getId().getId(), mobileApp, MobileApp.class);
MobileAppInfo retrievedMobileAppInfo = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId());
assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppInfo(savedMobileApp, List.of(new OAuth2ClientInfo(savedOAuth2Client))));
public void testGetTenantAppsByPlatformTypeSaveMobileApp() throws Exception {
MobileApp androidApp = doPost("/api/mobile/app", validMobileApp("android.1", PlatformType.ANDROID), MobileApp.class);
MobileApp androidApp2 = doPost("/api/mobile/app", validMobileApp("android.2", PlatformType.ANDROID), MobileApp.class);
MobileApp iosApp = doPost("/api/mobile/app", validMobileApp("ios.1", PlatformType.IOS), MobileApp.class);
PageData<MobileApp> pageData = doGetTypedWithPageLink("/api/mobile/app?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0));
assertThat(pageData.getData()).hasSize(3);
assertThat(pageData.getData()).containsExactlyInAnyOrder(androidApp, androidApp2, iosApp);
PageData<MobileApp> androidPageData = doGetTypedWithPageLink("/api/mobile/app?platformType=ANDROID&", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0));
assertThat(androidPageData.getData()).hasSize(2);
assertThat(androidPageData.getData()).containsExactlyInAnyOrder(androidApp, androidApp2);
}
private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, boolean oauth2Enabled) {
MobileApp MobileApp = new MobileApp();
MobileApp.setTenantId(tenantId);
MobileApp.setPkgName(mobileAppName);
MobileApp.setAppSecret(StringUtils.randomAlphanumeric(24));
MobileApp.setOauth2Enabled(oauth2Enabled);
return MobileApp;
private MobileApp validMobileApp(String mobileAppName, PlatformType platformType) {
MobileApp mobileApp = new MobileApp();
mobileApp.setPkgName(mobileAppName);
mobileApp.setAppSecret(StringUtils.randomAlphanumeric(24));
mobileApp.setPlatformType(platformType);
mobileApp.setStatus(MobileAppStatus.DRAFT);
return mobileApp;
}
}

253
application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java

@ -1,253 +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.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.hamcrest.Matchers.containsString;
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();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
QRCodeConfig qrCodeConfig = new QRCodeConfig();
qrCodeConfig.setQrCodeLabel(TEST_LABEL);
mobileAppSettings.setUseDefaultApp(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)
.andExpect(status().isOk());
}
@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.isUseDefaultApp()).isTrue();
mobileAppSettings.setUseDefaultApp(false);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
MobileAppSettings updatedMobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
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 configuration 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);
mobileAppSettings.setUseDefaultApp(false);
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 custom android application!")));
androidConfig.setAppPackage("test_app_package");
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for custom android application!")));
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);
mobileAppSettings.setUseDefaultApp(false);
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 custom ios application!")));
iosConfig.setAppId("test_app_id");
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
}
@Test
public void testShouldSaveMobileAppSettingsForDefaultApp() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
mobileAppSettings.setUseDefaultApp(true);
mobileAppSettings.setIosConfig(null);
mobileAppSettings.setAndroidConfig(null);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
}
@Test
public void testGetApplicationAssociations() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
mobileAppSettings.setUseDefaultApp(false);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
JsonNode assetLinks = doGet("/.well-known/assetlinks.json", JsonNode.class);
assertThat(assetLinks.get(0).get("target").get("package_name").asText()).isEqualTo(ANDROID_PACKAGE_NAME);
assertThat(assetLinks.get(0).get("target").get("sha256_cert_fingerprints").get(0).asText()).isEqualTo(ANDROID_APP_SHA256);
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 appHost = parsedDeepLink.group(1);
String secret = parsedDeepLink.group(2);
String ttl = parsedDeepLink.group(3);
assertThat(appHost).isEqualTo("demo.thingsboard.io");
assertThat(ttl).isEqualTo(String.valueOf(mobileSecretKeyTtl));
JwtPair jwtPair = doGet("/api/noauth/qr/" + secret, JwtPair.class);
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();
// update mobile setting to use custom one
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
mobileAppSettings.setUseDefaultApp(false);
doPost("/api/mobile/app/settings", mobileAppSettings);
String customAppDeepLink = doGet("/api/mobile/deepLink", String.class);
Pattern customAppExpectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)\"");
Matcher customAppParsedDeepLink = customAppExpectedPattern.matcher(customAppDeepLink);
assertThat(customAppParsedDeepLink.matches()).isTrue();
assertThat(customAppParsedDeepLink.group(1)).isEqualTo("localhost");
loginTenantAdmin();
String tenantCustomAppDeepLink = doGet("/api/mobile/deepLink", String.class);
Matcher tenantCustomAppParsedDeepLink = customAppExpectedPattern.matcher(tenantCustomAppDeepLink);
assertThat(tenantCustomAppParsedDeepLink.matches()).isTrue();
assertThat(tenantCustomAppParsedDeepLink.group(1)).isEqualTo("localhost");
loginCustomerUser();
String customerCustomAppDeepLink = doGet("/api/mobile/deepLink", String.class);
Matcher customerCustomAppParsedDeepLink = customAppExpectedPattern.matcher(customerCustomAppDeepLink);
assertThat(customerCustomAppParsedDeepLink.matches()).isTrue();
assertThat(customerCustomAppParsedDeepLink.group(1)).isEqualTo("localhost");
}
}

262
application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java

@ -0,0 +1,262 @@
/**
* 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.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.server.common.data.StringUtils;
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.mobile.bundle.MobileAppBundleInfo;
import org.thingsboard.server.common.data.mobile.qrCodeSettings.QRCodeConfig;
import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings;
import org.thingsboard.server.common.data.mobile.app.StoreInfo;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
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.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
@DaoSqlTest
public class QrCodeSettingsControllerTest extends AbstractControllerTest {
@Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}")
private int mobileSecretKeyTtl;
static final TypeReference<PageData<MobileAppBundleInfo>> PAGE_DATA_MOBILE_APP_BUNDLE_TYPE_REF = new TypeReference<>() {
};
static final TypeReference<PageData<MobileApp>> PAGE_DATA_MOBILE_APP_TYPE_REF = new TypeReference<>() {
};
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 ANDROID_STORE_LINK = "https://store.link.com";
private static final String APPLE_APP_ID = "testId";
private static final String TEST_LABEL = "Test label";
private static final String IOS_STORE_LINK = "https://store.link.com";
private MobileAppBundle mobileAppBundle;
@Before
public void setUp() throws Exception {
loginSysAdmin();
MobileApp androidApp = validMobileApp( "my.android.package", PlatformType.ANDROID);
StoreInfo androidStoreInfo = StoreInfo.builder()
.sha256CertFingerprints(ANDROID_APP_SHA256)
.storeLink(ANDROID_STORE_LINK)
.build();
androidApp.setStoreInfo(androidStoreInfo);
MobileApp savedAndroidApp = doPost("/api/mobile/app", androidApp, MobileApp.class);
MobileApp iosApp = validMobileApp( "my.ios.package", PlatformType.IOS);
StoreInfo iosQrCodeConfig = StoreInfo.builder()
.appId(APPLE_APP_ID)
.storeLink(IOS_STORE_LINK)
.build();
iosApp.setStoreInfo(iosQrCodeConfig);
MobileApp savedIosApp = doPost("/api/mobile/app", iosApp, MobileApp.class);
mobileAppBundle = new MobileAppBundle();
mobileAppBundle.setTitle("Test bundle");
mobileAppBundle.setAndroidAppId(savedAndroidApp.getId());
mobileAppBundle.setIosAppId(savedIosApp.getId());
mobileAppBundle = doPost("/api/mobile/bundle", mobileAppBundle, MobileAppBundle.class);
QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class);
QRCodeConfig qrCodeConfig = new QRCodeConfig();
qrCodeConfig.setQrCodeLabel(TEST_LABEL);
qrCodeSettings.setUseDefaultApp(true);
qrCodeSettings.setMobileAppBundleId(null);
qrCodeSettings.setQrCodeConfig(qrCodeConfig);
doPost("/api/mobile/qr/settings", qrCodeSettings)
.andExpect(status().isOk());
}
@After
public void tearDown() throws Exception {
loginSysAdmin();
PageData<MobileAppBundleInfo> pageData2 = doGetTypedWithPageLink("/api/mobile/bundle/infos?", PAGE_DATA_MOBILE_APP_BUNDLE_TYPE_REF, new PageLink(10, 0));
for (MobileAppBundleInfo appBundleInfo : pageData2.getData()) {
doDelete("/api/mobile/bundle/" + appBundleInfo.getId().getId())
.andExpect(status().isOk());
}
PageData<MobileApp> pageData = doGetTypedWithPageLink("/api/mobile/app?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0));
for (MobileApp mobileApp : pageData.getData()) {
doDelete("/api/mobile/app/" + mobileApp.getId().getId())
.andExpect(status().isOk());
}
}
@Test
public void testSaveQrCodeSettings() throws Exception {
loginSysAdmin();
QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class);
assertThat(qrCodeSettings.getQrCodeConfig().getQrCodeLabel()).isEqualTo(TEST_LABEL);
assertThat(qrCodeSettings.isUseDefaultApp()).isTrue();
qrCodeSettings.setUseDefaultApp(false);
qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId());
doPost("/api/mobile/qr/settings", qrCodeSettings)
.andExpect(status().isOk());
QrCodeSettings updatedQrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class);
assertThat(updatedQrCodeSettings.isUseDefaultApp()).isFalse();
}
@Test
public void testShouldNotSaveQrCodeSettingsWithoutRequiredConfig() throws Exception {
loginSysAdmin();
QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class);
qrCodeSettings.setUseDefaultApp(false);
qrCodeSettings.setQrCodeConfig(null);
qrCodeSettings.setMobileAppBundleId(null);
doPost("/api/mobile/qr/settings", qrCodeSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Validation error: qrCodeConfig must not be null")));
qrCodeSettings.setQrCodeConfig(QRCodeConfig.builder().showOnHomePage(false).build());
doPost("/api/mobile/qr/settings", qrCodeSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Mobile app bundle is required to use custom application!")));
qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId());
doPost("/api/mobile/qr/settings", qrCodeSettings)
.andExpect(status().isOk());
}
@Test
public void testShouldSaveQrCodeSettingsForDefaultApp() throws Exception {
loginSysAdmin();
QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class);
qrCodeSettings.setUseDefaultApp(true);
qrCodeSettings.setMobileAppBundleId(null);
doPost("/api/mobile/qr/settings", qrCodeSettings)
.andExpect(status().isOk());
}
@Test
public void testGetApplicationAssociations() throws Exception {
loginSysAdmin();
QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class);
qrCodeSettings.setUseDefaultApp(true);
qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId());
doPost("/api/mobile/qr/settings", qrCodeSettings)
.andExpect(status().isOk());
JsonNode assetLinks = doGet("/.well-known/assetlinks.json", JsonNode.class);
assertThat(assetLinks.get(0).get("target").get("package_name").asText()).isEqualTo("my.android.package");
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/qr/deepLink", String.class);
Pattern expectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)&host=([^&]+)\"");
Matcher parsedDeepLink = expectedPattern.matcher(deepLink);
assertThat(parsedDeepLink.matches()).isTrue();
String appHost = parsedDeepLink.group(1);
String secret = parsedDeepLink.group(2);
String ttl = parsedDeepLink.group(3);
assertThat(appHost).isEqualTo("demo.thingsboard.io");
assertThat(ttl).isEqualTo(String.valueOf(mobileSecretKeyTtl));
JwtPair jwtPair = doGet("/api/noauth/qr/" + secret, JwtPair.class);
assertThat(jwtPair).isNotNull();
loginTenantAdmin();
String tenantDeepLink = doGet("/api/mobile/qr/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/qr/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();
// update mobile setting to use custom one
loginSysAdmin();
QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class);
qrCodeSettings.setUseDefaultApp(false);
qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId());
doPost("/api/mobile/qr/settings", qrCodeSettings);
String customAppDeepLink = doGet("/api/mobile/qr/deepLink", String.class);
Pattern customAppExpectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)\"");
Matcher customAppParsedDeepLink = customAppExpectedPattern.matcher(customAppDeepLink);
assertThat(customAppParsedDeepLink.matches()).isTrue();
assertThat(customAppParsedDeepLink.group(1)).isEqualTo("localhost");
loginTenantAdmin();
String tenantCustomAppDeepLink = doGet("/api/mobile/qr/deepLink", String.class);
Matcher tenantCustomAppParsedDeepLink = customAppExpectedPattern.matcher(tenantCustomAppDeepLink);
assertThat(tenantCustomAppParsedDeepLink.matches()).isTrue();
assertThat(tenantCustomAppParsedDeepLink.group(1)).isEqualTo("localhost");
loginCustomerUser();
String customerCustomAppDeepLink = doGet("/api/mobile/qr/deepLink", String.class);
Matcher customerCustomAppParsedDeepLink = customAppExpectedPattern.matcher(customerCustomAppDeepLink);
assertThat(customerCustomAppParsedDeepLink.matches()).isTrue();
assertThat(customerCustomAppParsedDeepLink.group(1)).isEqualTo("localhost");
}
private MobileApp validMobileApp(String mobileAppName, PlatformType platformType) {
MobileApp mobileApp = new MobileApp();
mobileApp.setTenantId(tenantId);
mobileApp.setStatus(MobileAppStatus.PUBLISHED);
mobileApp.setPkgName(mobileAppName);
mobileApp.setPlatformType(platformType);
mobileApp.setAppSecret(StringUtils.randomAlphanumeric(24));
StoreInfo storeInfo = StoreInfo.builder()
.storeLink("https://play.google/test")
.sha256CertFingerprints(ANDROID_APP_SHA256)
.appId("test.app.id").build();
mobileApp.setStoreInfo(storeInfo);
return mobileApp;
}
}

44
application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device;
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.AlarmSeverity;
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.DeviceId;
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.RuleNodeId;
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.StringDataEntry;
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.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
@ -233,6 +242,27 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
tenantId = differentTenantId;
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();
createRelatedData(differentTenantCustomerId);
loginDifferentTenant();
@ -279,6 +309,10 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
verifyNoRelatedData(userId);
verifyNoRelatedData(differentTenantCustomerId);
verifyNoRelatedData(tenantApiUsageState.getId());
verifyNoRelatedData(androidAppId);
verifyNoRelatedData(iosAppId);
verifyNoRelatedData(oAuth2ClientId);
verifyNoRelatedData(appBundleId);
verifyNoRelatedData(tenantId);
});
}
@ -483,4 +517,14 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
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;
}
}

46
common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.mobile;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.id.TenantId;
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.PlatformType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.List;
public interface MobileAppBundleService extends EntityDaoService {
MobileAppBundle saveMobileAppBundle(TenantId tenantId, MobileAppBundle mobileAppBundle);
void updateOauth2Clients(TenantId tenantId, MobileAppBundleId mobileAppBundleId, List<OAuth2ClientId> oAuth2ClientIds);
MobileAppBundle findMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId);
PageData<MobileAppBundleInfo> findMobileAppBundleInfosByTenantId(TenantId tenantId, PageLink pageLink);
MobileAppBundleInfo findMobileAppBundleInfoById(TenantId tenantId, MobileAppBundleId mobileAppBundleId);
MobileAppBundle findMobileAppBundleByPkgNameAndPlatform(TenantId tenantId, String pkgName, PlatformType platform);
void deleteMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId);
}

19
common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java

@ -15,30 +15,27 @@
*/
package org.thingsboard.server.dao.mobile;
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.TenantId;
import org.thingsboard.server.common.data.mobile.MobileApp;
import org.thingsboard.server.common.data.mobile.MobileAppInfo;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.List;
public interface MobileAppService extends EntityDaoService {
MobileApp saveMobileApp(TenantId tenantId, MobileApp mobileApp);
void deleteMobileAppById(TenantId tenantId, MobileAppId mobileAppId);
MobileApp findMobileAppById(TenantId tenantId, MobileAppId mobileAppId);
PageData<MobileAppInfo> findMobileAppInfosByTenantId(TenantId tenantId, PageLink pageLink);
PageData<MobileApp> findMobileAppsByTenantId(TenantId tenantId, PlatformType platformType, PageLink pageLink);
MobileAppInfo findMobileAppInfoById(TenantId tenantId, MobileAppId mobileAppId);
MobileApp findByBundleIdAndPlatformType(TenantId tenantId, MobileAppBundleId mobileAppBundleId, PlatformType platformType);
void updateOauth2Clients(TenantId tenantId, MobileAppId mobileAppId, List<OAuth2ClientId> oAuth2ClientIds);
MobileApp findMobileAppByPkgNameAndPlatformType(String pkgName, PlatformType platform);
void deleteMobileAppById(TenantId tenantId, MobileAppId mobileAppId);
void deleteMobileAppsByTenantId(TenantId tenantId);
}

2
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -51,6 +51,6 @@ public class CacheConstants {
public static final String ENTITY_COUNT_CACHE = "entityCount";
public static final String RESOURCE_INFO_CACHE = "resourceInfo";
public static final String ALARM_TYPES_CACHE = "alarmTypes";
public static final String MOBILE_APP_SETTINGS_CACHE = "mobileAppSettings";
public static final String QR_CODE_SETTINGS_CACHE = "qrCodeSettings";
public static final String MOBILE_SECRET_KEY_CACHE = "mobileSecretKey";
}

3
common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java

@ -60,7 +60,8 @@ public enum EntityType {
QUEUE_STATS(34),
OAUTH2_CLIENT(35),
DOMAIN(36),
MOBILE_APP(37);
MOBILE_APP(37),
MOBILE_APP_BUNDLE(38);
@Getter
private final int protoNumber; // Corresponds to EntityTypeProto

21
common/data/src/main/java/org/thingsboard/server/common/data/Views.java

@ -0,0 +1,21 @@
/**
* 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;
public class Views {
public static class Public {}
public static class Private extends Public {}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java

@ -111,6 +111,8 @@ public class EntityIdFactory {
return new MobileAppId(uuid);
case DOMAIN:
return new DomainId(uuid);
case MOBILE_APP_BUNDLE:
return new MobileAppBundleId(uuid);
}
throw new IllegalArgumentException("EntityType " + type + " is not supported!");
}

39
common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java

@ -0,0 +1,39 @@
/**
* 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 org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
public class MobileAppBundleId extends UUIDBased implements EntityId{
@JsonCreator
public MobileAppBundleId(@JsonProperty("id") UUID id) {
super(id);
}
public static MobileAppBundleId fromString(String mobileAppId) {
return new MobileAppBundleId(UUID.fromString(mobileAppId));
}
@Override
public EntityType getEntityType() {
return EntityType.MOBILE_APP_BUNDLE;
}
}

8
common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java → common/data/src/main/java/org/thingsboard/server/common/data/id/QrCodeSettingsId.java

@ -22,16 +22,16 @@ import io.swagger.v3.oas.annotations.media.Schema;
import java.util.UUID;
@Schema
public class MobileAppSettingsId extends UUIDBased {
public class QrCodeSettingsId extends UUIDBased {
private static final long serialVersionUID = 1L;
@JsonCreator
public MobileAppSettingsId(@JsonProperty("id") UUID id) {
public QrCodeSettingsId(@JsonProperty("id") UUID id) {
super(id);
}
public static MobileAppSettingsId fromString(String mobileAppSettingsId) {
return new MobileAppSettingsId(UUID.fromString(mobileAppSettingsId));
public static QrCodeSettingsId fromString(String qrCodeSettingsId) {
return new QrCodeSettingsId(UUID.fromString(qrCodeSettingsId));
}
}

24
common/data/src/main/java/org/thingsboard/server/common/data/mobile/LoginMobileInfo.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;
import org.thingsboard.server.common.data.mobile.app.MobileAppVersionInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo;
import java.util.List;
public record LoginMobileInfo(List<OAuth2ClientLoginInfo> oAuth2ClientLoginInfos, MobileAppVersionInfo versionInfo) {
}

11
common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java

@ -15,13 +15,10 @@
*/
package org.thingsboard.server.common.data.mobile;
import lombok.Data;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.HomeDashboardInfo;
import org.thingsboard.server.common.data.User;
import java.util.Map;
@Data
public class UserMobileInfo {
private Map<String, MobileSessionInfo> sessions;
public record UserMobileInfo(User user, HomeDashboardInfo homeDashboardInfo, JsonNode pages) {
}

27
common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileSessionInfo.java

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.mobile;
import lombok.Data;
import java.util.Map;
@Data
public class UserMobileSessionInfo {
private Map<String, MobileSessionInfo> sessions;
}

24
common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java → common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileApp.java

@ -13,12 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.mobile;
package org.thingsboard.server.common.data.mobile.app;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ -27,6 +29,7 @@ import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.validation.Length;
@EqualsAndHashCode(callSuper = true)
@ -44,8 +47,18 @@ public class MobileApp extends BaseData<MobileAppId> implements HasTenantId, Has
@NotEmpty
@Length(fieldName = "appSecret", min = 16, max = 2048, message = "must be at least 16 and max 2048 characters")
private String appSecret;
@Schema(description = "Whether OAuth2 settings are enabled or not")
private boolean oauth2Enabled;
@Schema(description = "Application platform type: ANDROID or IOS", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull
private PlatformType platformType;
@Schema(description = "Application status: PUBLISHED, DEPRECATED, SUSPENDED, DRAFT", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull
private MobileAppStatus status;
@Schema(description = "Application version info")
@Valid
private MobileAppVersionInfo versionInfo;
@Schema(description = "Application store information")
@Valid
private StoreInfo storeInfo;
public MobileApp() {
super();
@ -60,7 +73,10 @@ public class MobileApp extends BaseData<MobileAppId> implements HasTenantId, Has
this.tenantId = mobile.tenantId;
this.pkgName = mobile.pkgName;
this.appSecret = mobile.appSecret;
this.oauth2Enabled = mobile.oauth2Enabled;
this.platformType = mobile.platformType;
this.status = mobile.status;
this.versionInfo = mobile.versionInfo;
this.storeInfo = mobile.storeInfo;
}
@Override

25
common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppStatus.java

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.mobile.app;
public enum MobileAppStatus {
DRAFT,
PUBLISHED,
DEPRECATED,
SUSPENDED
}

49
common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppVersionInfo.java

@ -0,0 +1,49 @@
/**
* 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.app;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.validation.Length;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class MobileAppVersionInfo {
@Schema(description = "Minimum supported version")
@Length(fieldName = "minVersion", max = 20)
private String minVersion;
@Schema(description = "Release notes of minimum supported version")
@Length(fieldName = "minVersionReleaseNotes", max = 40000)
private String minVersionReleaseNotes;
@Schema(description = "Latest supported version")
@Length(fieldName = "latestVersion", max = 20)
private String latestVersion;
@Schema(description = "Release notes of latest supported version")
@Length(fieldName = "latestVersionReleaseNotes", max = 40000)
private String latestVersionReleaseNotes;
}

13
common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/StoreInfo.java

@ -13,26 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.mobile;
package org.thingsboard.server.common.data.mobile.app;
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 {
public class StoreInfo {
private boolean enabled;
@NoXss
private String appId;
@NoXss
private String sha256CertFingerprints;
@NoXss
private String storeLink;
}

83
common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java

@ -0,0 +1,83 @@
/**
* 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.bundle;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig;
import org.thingsboard.server.common.data.validation.Length;
@EqualsAndHashCode(callSuper = true)
@Data
@ToString
public class MobileAppBundle extends BaseData<MobileAppBundleId> implements HasTenantId, HasName {
@Schema(description = "JSON object with Tenant Id")
private TenantId tenantId;
@Schema(description = "Application bundle title. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank
@Length(fieldName = "title")
private String title;
@Schema(description = "Application bundle description.")
@Length(fieldName = "description")
private String description;
@Schema(description = "Android application id")
private MobileAppId androidAppId;
@Schema(description = "IOS application id")
private MobileAppId iosAppId;
@Schema(description = "Application layout configuration")
@Valid
private MobileLayoutConfig layoutConfig;
@Schema(description = "Whether OAuth2 settings are enabled or not")
private Boolean oauth2Enabled;
public MobileAppBundle() {
super();
}
public MobileAppBundle(MobileAppBundleId id) {
super(id);
}
public MobileAppBundle(MobileAppBundle mobile) {
super(mobile);
this.tenantId = mobile.tenantId;
this.title = mobile.title;
this.description = mobile.description;
this.androidAppId = mobile.androidAppId;
this.iosAppId = mobile.iosAppId;
this.layoutConfig = mobile.layoutConfig;
this.oauth2Enabled = mobile.oauth2Enabled;
}
@Override
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
@Schema(description = "Mobile app bundle title", example = "My main application", accessMode = Schema.AccessMode.READ_ONLY)
public String getName() {
return title;
}
}

63
common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundleInfo.java

@ -0,0 +1,63 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.mobile.bundle;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
@Schema
public class MobileAppBundleInfo extends MobileAppBundle {
@Schema(description = "Android package name")
private String androidPkgName;
@Schema(description = "IOS package name")
private String iosPkgName;
@Schema(description = "List of available oauth2 clients")
private List<OAuth2ClientInfo> oauth2ClientInfos;
@Schema(description = "Indicates if qr code is available for bundle")
private boolean qrCodeEnabled;
public MobileAppBundleInfo(MobileAppBundle mobileApp, String androidPkgName, String iosPkgName, boolean qrCodeEnabled) {
super(mobileApp);
this.androidPkgName = androidPkgName;
this.iosPkgName = iosPkgName;
this.qrCodeEnabled = qrCodeEnabled;
}
public MobileAppBundleInfo(MobileAppBundle mobileApp, String androidPkgName, String iosPkgName, boolean qrCodeEnabled, List<OAuth2ClientInfo> oauth2ClientInfos) {
super(mobileApp);
this.androidPkgName = androidPkgName;
this.iosPkgName = iosPkgName;
this.qrCodeEnabled = qrCodeEnabled;
this.oauth2ClientInfos = oauth2ClientInfos;
}
public MobileAppBundleInfo() {
super();
}
public MobileAppBundleInfo(MobileAppBundleId mobileAppBundleId) {
super(mobileAppBundleId);
}
}

8
common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppOauth2Client.java → common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundleOauth2Client.java

@ -13,20 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.mobile;
package org.thingsboard.server.common.data.mobile.bundle;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MobileAppOauth2Client {
public class MobileAppBundleOauth2Client {
private MobileAppId mobileAppId;
private MobileAppBundleId mobileAppBundleId;
private OAuth2ClientId oAuth2ClientId;
}

35
common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java

@ -0,0 +1,35 @@
/**
* 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.layout;
import com.fasterxml.jackson.annotation.JsonView;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.thingsboard.server.common.data.Views;
@Data
public abstract class AbstractMobilePage implements MobilePage {
@Schema(description = "Page label", example = "Air quality")
@JsonView(Views.Public.class)
protected String label;
@Schema(description = "Indicates if page is visible", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
@JsonView(Views.Private.class)
protected boolean visible;
@Schema(description = "URL of the page icon", example = "home_icon")
@JsonView(Views.Public.class)
protected String icon;
}

42
common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java

@ -0,0 +1,42 @@
/**
* 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.layout;
import com.fasterxml.jackson.annotation.JsonView;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.Views;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class CustomMobilePage extends AbstractMobilePage {
@Schema(description = "Path to custom page", example = "/alarmDetails/868c7083-032d-4f52-b8b4-7859aebb6a4e")
@JsonView(Views.Public.class)
private String path;
@Override
public MobilePageType getType() {
return MobilePageType.CUSTOM;
}
}

42
common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java

@ -0,0 +1,42 @@
/**
* 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.layout;
import com.fasterxml.jackson.annotation.JsonView;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.Views;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class DashboardPage extends AbstractMobilePage {
@Schema(description = "Dashboard id", example = "784f394c-42b6-435a-983c-b7beff2784f9")
@JsonView(Views.Public.class)
private String dashboardId;
@Override
public MobilePageType getType() {
return MobilePageType.DASHBOARD;
}
}

43
common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java

@ -0,0 +1,43 @@
/**
* 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.layout;
import com.fasterxml.jackson.annotation.JsonView;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.Views;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class DefaultMobilePage extends AbstractMobilePage {
@Schema(description = "Identifier for default page", example = "HOME")
@JsonView(Views.Public.class)
private DefaultPageId id;
@Override
public MobilePageType getType() {
return MobilePageType.DEFAULT;
}
}

29
common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultPageId.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.mobile.layout;
public enum DefaultPageId {
HOME,
ALARMS,
DEVICES,
CUSTOMERS,
ASSETS,
AUDIT_LOGS,
NOTIFICATIONS,
DEVICE_LIST,
DASHBOARDS
}

23
common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java

@ -13,28 +13,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.mobile;
package org.thingsboard.server.common.data.mobile.layout;
import com.fasterxml.jackson.annotation.JsonView;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.validation.NoXss;
import org.thingsboard.server.common.data.Views;
import java.util.ArrayList;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class AndroidConfig {
public class MobileLayoutConfig {
private boolean enabled;
@NoXss
private String appPackage;
@NoXss
private String sha256CertFingerprints;
@NoXss
private String storeLink;
@Schema(description = "List of pages")
@JsonView(Views.Public.class)
@Valid
private List<MobilePage> pages = new ArrayList<>();
}

45
common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.mobile.layout;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonView;
import org.thingsboard.server.common.data.Views;
import java.io.Serializable;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = DefaultMobilePage.class, name = "DEFAULT"),
@JsonSubTypes.Type(value = DashboardPage.class, name = "DASHBOARD"),
@JsonSubTypes.Type(value = WebViewPage.class, name = "WEB_VIEW"),
@JsonSubTypes.Type(value = CustomMobilePage.class, name = "CUSTOM")
})
public interface MobilePage extends Serializable {
@JsonView(Views.Private.class)
MobilePageType getType();
@JsonView(Views.Private.class)
boolean isVisible();
}

24
common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePageType.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.layout;
public enum MobilePageType {
DEFAULT,
DASHBOARD,
WEB_VIEW,
CUSTOM
}

42
common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java

@ -0,0 +1,42 @@
/**
* 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.layout;
import com.fasterxml.jackson.annotation.JsonView;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.Views;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class WebViewPage extends AbstractMobilePage {
@Schema(description = "Url", example = "/url")
@JsonView(Views.Public.class)
private String url;
@Override
public MobilePageType getType() {
return MobilePageType.WEB_VIEW;
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java → common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/BadgePosition.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.mobile;
package org.thingsboard.server.common.data.mobile.qrCodeSettings;
public enum BadgePosition {

3
common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QRCodeConfig.java

@ -13,13 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.mobile;
package org.thingsboard.server.common.data.mobile.qrCodeSettings;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.mobile.qrCodeSettings.BadgePosition;
import org.thingsboard.server.common.data.validation.NoXss;
@Data

37
common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java → common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QrCodeSettings.java

@ -13,49 +13,52 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.mobile;
package org.thingsboard.server.common.data.mobile.qrCodeSettings;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
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.MobileAppBundleId;
import org.thingsboard.server.common.data.id.QrCodeSettingsId;
import org.thingsboard.server.common.data.id.TenantId;
@Schema
@Data
@EqualsAndHashCode(callSuper = true)
public class MobileAppSettings extends BaseData<MobileAppSettingsId> implements HasTenantId {
public class QrCodeSettings extends BaseData<QrCodeSettingsId> implements HasTenantId {
private static final long serialVersionUID = 2628323657987010348L;
@Schema(description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY)
private TenantId tenantId;
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Type of application: true means use default Thingsboard app", example = "true")
@Schema(description = "Use settings from system level", example = "true")
private boolean useSystemSettings;
@Schema(description = "Type of application: true means use default Thingsboard app", example = "true")
private boolean useDefaultApp;
@Valid
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Android mobile app configuration.")
private AndroidConfig androidConfig;
@Valid
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Ios mobile app configuration.")
private IosConfig iosConfig;
@Valid
@Schema(description = "Mobile app bundle.")
private MobileAppBundleId mobileAppBundleId;
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "QR code config configuration.")
@Valid
@NotNull
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)
private String defaultGooglePlayLink;
private String googlePlayLink;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String defaultAppStoreLink;
private String appStoreLink;
public MobileAppSettings() {
public QrCodeSettings() {
}
public MobileAppSettings(MobileAppSettingsId id) {
public QrCodeSettings(QrCodeSettingsId id) {
super(id);
}

1
common/proto/src/main/proto/queue.proto

@ -58,6 +58,7 @@ enum EntityTypeProto {
OAUTH2_CLIENT = 35;
DOMAIN = 36;
MOBILE_APP = 37;
MOBILE_APP_BUNDLE = 38;
}
/**

13
common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java

@ -35,6 +35,7 @@ import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.Views;
import java.io.File;
import java.io.IOException;
@ -174,6 +175,10 @@ public class JacksonUtil {
}
}
public static String writeValueAsViewIgnoringNullFields(Object value, Class<Views.Public> serializationView) throws JsonProcessingException {
return value == null ? "" : OBJECT_MAPPER.writerWithView(serializationView).writeValueAsString(value);
}
public static String toPrettyString(Object o) {
try {
return PRETTY_SORTED_JSON_MAPPER.writeValueAsString(o);
@ -227,6 +232,14 @@ public class JacksonUtil {
}
}
public static <T> T readValue(String object, TypeReference<T> clazz) {
try {
return OBJECT_MAPPER.readValue(object, clazz);
} catch (IOException e) {
throw new IllegalArgumentException("Can't read object: " + object, e);
}
}
public static <T> T readValue(File file, TypeReference<T> clazz) {
try {
return OBJECT_MAPPER.readValue(file, clazz);

120
dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java

@ -1,120 +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.dao.mobile;
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.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.AndroidConfig;
import org.thingsboard.server.common.data.mobile.BadgePosition;
import org.thingsboard.server.common.data.mobile.IosConfig;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.mobile.QRCodeConfig;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.service.DataValidator;
import java.util.Map;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service
@Slf4j
@RequiredArgsConstructor
public class BaseMobileAppSettingsService extends AbstractCachedEntityService<TenantId, MobileAppSettings, MobileAppSettingsEvictEvent> 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";
@Value("${mobileApp.googlePlayLink:https://play.google.com/store/apps/details?id=org.thingsboard.demo.app}")
private String googlePlayLink;
@Value("${mobileApp.appStoreLink:https://apps.apple.com/us/app/thingsboard-live/id1594355695}")
private String appStoreLink;
private final MobileAppSettingsDao mobileAppSettingsDao;
private final DataValidator<MobileAppSettings> mobileAppSettingsDataValidator;
@Override
public MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings mobileAppSettings) {
mobileAppSettingsDataValidator.validate(mobileAppSettings, s -> tenantId);
try {
MobileAppSettings savedMobileAppSettings = mobileAppSettingsDao.save(tenantId, mobileAppSettings);
publishEvictEvent(new MobileAppSettingsEvictEvent(tenantId));
return constructMobileAppSettings(savedMobileAppSettings);
} catch (Exception e) {
handleEvictEvent(new MobileAppSettingsEvictEvent(tenantId));
checkConstraintViolation(e, Map.of(
"mobile_app_settings_tenant_id_unq_key", "Mobile application for specified tenant already exists!"
));
throw e;
}
}
@Override
public MobileAppSettings getMobileAppSettings(TenantId tenantId) {
log.trace("Executing getMobileAppSettings for tenant [{}] ", tenantId);
MobileAppSettings mobileAppSettings = cache.getAndPutInTransaction(tenantId,
() -> mobileAppSettingsDao.findByTenantId(tenantId), true);
return constructMobileAppSettings(mobileAppSettings);
}
@Override
public void deleteByTenantId(TenantId tenantId) {
log.trace("Executing deleteByTenantId, tenantId [{}]", tenantId);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
mobileAppSettingsDao.removeByTenantId(tenantId);
}
@TransactionalEventListener(classes = MobileAppSettingsEvictEvent.class)
@Override
public void handleEvictEvent(MobileAppSettingsEvictEvent event) {
cache.evict(event.getTenantId());
}
private MobileAppSettings constructMobileAppSettings(MobileAppSettings mobileAppSettings) {
if (mobileAppSettings == null) {
mobileAppSettings = new MobileAppSettings();
mobileAppSettings.setUseDefaultApp(true);
AndroidConfig androidConfig = AndroidConfig.builder()
.enabled(true)
.build();
IosConfig iosConfig = IosConfig.builder()
.enabled(true)
.build();
QRCodeConfig qrCodeConfig = QRCodeConfig.builder()
.showOnHomePage(true)
.qrCodeLabelEnabled(true)
.qrCodeLabel(DEFAULT_QR_CODE_LABEL)
.badgeEnabled(true)
.badgePosition(BadgePosition.RIGHT)
.badgeEnabled(true)
.build();
mobileAppSettings.setQrCodeConfig(qrCodeConfig);
mobileAppSettings.setAndroidConfig(androidConfig);
mobileAppSettings.setIosConfig(iosConfig);
}
if (mobileAppSettings.isUseDefaultApp()) {
mobileAppSettings.setDefaultGooglePlayLink(googlePlayLink);
mobileAppSettings.setDefaultAppStoreLink(appStoreLink);
}
return mobileAppSettings;
}
}

46
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.java

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.mobile;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleOauth2Client;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
import java.util.List;
public interface MobileAppBundleDao extends Dao<MobileAppBundle> {
PageData<MobileAppBundleInfo> findInfosByTenantId(TenantId tenantId, PageLink pageLink);
MobileAppBundleInfo findInfoById(TenantId tenantId, MobileAppBundleId mobileAppBundleId);
List<MobileAppBundleOauth2Client> findOauth2ClientsByMobileAppBundleId(TenantId tenantId, MobileAppBundleId mobileAppBundleId);
void addOauth2Client(TenantId tenantId, MobileAppBundleOauth2Client mobileAppBundleOauth2Client);
void removeOauth2Client(TenantId tenantId, MobileAppBundleOauth2Client mobileAppBundleOauth2Client);
MobileAppBundle findByPkgNameAndPlatform(TenantId tenantId, String pkgName, PlatformType platform);
void deleteByTenantId(TenantId tenantId);
}

170
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java

@ -0,0 +1,170 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.mobile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleOauth2Client;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.oauth2.OAuth2ClientDao;
import org.thingsboard.server.dao.service.DataValidator;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static org.thingsboard.server.dao.service.Validator.checkNotNull;
@Slf4j
@Service
public class MobileAppBundleServiceImpl extends AbstractEntityService implements MobileAppBundleService {
private static final String PLATFORM_TYPE_IS_REQUIRED = "Platform type is required if package name is specified";
@Autowired
private OAuth2ClientDao oauth2ClientDao;
@Autowired
private MobileAppBundleDao mobileAppBundleDao;
@Autowired
private DataValidator<MobileAppBundle> mobileAppBundleDataValidator;
@Override
public MobileAppBundle saveMobileAppBundle(TenantId tenantId, MobileAppBundle mobileAppBundle) {
log.trace("Executing saveMobileAppBundle [{}]", mobileAppBundle);
mobileAppBundleDataValidator.validate(mobileAppBundle, b -> tenantId);
try {
MobileAppBundle savedMobileApp = mobileAppBundleDao.save(tenantId, mobileAppBundle);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(savedMobileApp).build());
return savedMobileApp;
} catch (Exception e) {
checkConstraintViolation(e,
Map.of("android_app_id_unq_key", "Android mobile app already exists in another bundle!",
"ios_app_id_unq_key", "IOS mobile app already exists in another bundle!"));
throw e;
}
}
@Override
public void updateOauth2Clients(TenantId tenantId, MobileAppBundleId mobileAppBundleId, List<OAuth2ClientId> oAuth2ClientIds) {
log.trace("Executing updateOauth2Clients, mobileAppId [{}], oAuth2ClientIds [{}]", mobileAppBundleId, oAuth2ClientIds);
Set<MobileAppBundleOauth2Client> newClientList = oAuth2ClientIds.stream()
.map(clientId -> new MobileAppBundleOauth2Client(mobileAppBundleId, clientId))
.collect(Collectors.toSet());
List<MobileAppBundleOauth2Client> existingClients = mobileAppBundleDao.findOauth2ClientsByMobileAppBundleId(tenantId, mobileAppBundleId);
List<MobileAppBundleOauth2Client> toRemoveList = existingClients.stream()
.filter(client -> !newClientList.contains(client))
.toList();
newClientList.removeIf(existingClients::contains);
for (MobileAppBundleOauth2Client client : toRemoveList) {
mobileAppBundleDao.removeOauth2Client(tenantId, client);
}
for (MobileAppBundleOauth2Client client : newClientList) {
mobileAppBundleDao.addOauth2Client(tenantId, client);
}
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId)
.entityId(mobileAppBundleId).created(false).build());
}
@Override
public MobileAppBundle findMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId) {
log.trace("Executing findMobileAppBundleById [{}] [{}]", tenantId, mobileAppBundleId);
return mobileAppBundleDao.findById(tenantId, mobileAppBundleId.getId());
}
@Override
public PageData<MobileAppBundleInfo> findMobileAppBundleInfosByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findMobileAppBundleInfosByTenantId [{}]", tenantId);
PageData<MobileAppBundleInfo> mobileBundles = mobileAppBundleDao.findInfosByTenantId(tenantId, pageLink);
mobileBundles.getData().forEach(this::fetchOauth2Clients);
return mobileBundles;
}
@Override
public MobileAppBundleInfo findMobileAppBundleInfoById(TenantId tenantId, MobileAppBundleId mobileAppIdBundle) {
log.trace("Executing findMobileAppBundleInfoById [{}] [{}]", tenantId, mobileAppIdBundle);
MobileAppBundleInfo mobileAppBundleInfo = mobileAppBundleDao.findInfoById(tenantId, mobileAppIdBundle);
if (mobileAppBundleInfo != null) {
fetchOauth2Clients(mobileAppBundleInfo);
}
return mobileAppBundleInfo;
}
@Override
public MobileAppBundle findMobileAppBundleByPkgNameAndPlatform(TenantId tenantId, String pkgName, PlatformType platform) {
log.trace("Executing findMobileAppBundleByPkgNameAndPlatform, tenantId [{}], pkgName [{}], platform [{}]", tenantId, pkgName, platform);
checkNotNull(platform, PLATFORM_TYPE_IS_REQUIRED);
return mobileAppBundleDao.findByPkgNameAndPlatform(tenantId, pkgName, platform);
}
@Override
public void deleteMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId) {
log.trace("Executing deleteMobileAppBundleById [{}]", mobileAppBundleId.getId());
mobileAppBundleDao.removeById(tenantId, mobileAppBundleId.getId());
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(mobileAppBundleId).build());
}
@Override
public void deleteByTenantId(TenantId tenantId) {
log.trace("Executing deleteMobileAppsByTenantId, tenantId [{}]", tenantId);
mobileAppBundleDao.deleteByTenantId(tenantId);
}
@Override
public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) {
return Optional.ofNullable(findMobileAppBundleById(tenantId, new MobileAppBundleId(entityId.getId())));
}
@Override
@Transactional
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
deleteMobileAppBundleById(tenantId, (MobileAppBundleId) id);
}
@Override
public EntityType getEntityType() {
return EntityType.MOBILE_APP_BUNDLE;
}
private void fetchOauth2Clients(MobileAppBundleInfo mobileAppBundleInfo) {
List<OAuth2ClientInfo> clients = oauth2ClientDao.findByMobileAppBundleId(mobileAppBundleInfo.getUuidId()).stream()
.map(OAuth2ClientInfo::new)
.sorted(Comparator.comparing(OAuth2ClientInfo::getTitle))
.collect(Collectors.toList());
mobileAppBundleInfo.setOauth2ClientInfos(clients);
}
}

19
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java

@ -15,26 +15,21 @@
*/
package org.thingsboard.server.dao.mobile;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileApp;
import org.thingsboard.server.common.data.mobile.MobileAppOauth2Client;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
import java.util.List;
public interface MobileAppDao extends Dao<MobileApp> {
PageData<MobileApp> findByTenantId(TenantId tenantId, PageLink pageLink);
List<MobileAppOauth2Client> findOauth2ClientsByMobileAppId(TenantId tenantId, MobileAppId mobileAppId);
MobileApp findByBundleIdAndPlatformType(TenantId tenantId, MobileAppBundleId mobileAppBundleId, PlatformType platformType);
void addOauth2Client(MobileAppOauth2Client mobileAppOauth2Client);
void removeOauth2Client(MobileAppOauth2Client mobileAppOauth2Client);
PageData<MobileApp> findByTenantId(TenantId tenantId, PlatformType platformType, PageLink pageLink);
void deleteByTenantId(TenantId tenantId);
MobileApp findByPkgNameAndPlatformType(TenantId tenantId, String pkgName, PlatformType platform);
}

96
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java

@ -22,46 +22,45 @@ import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
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.TenantId;
import org.thingsboard.server.common.data.mobile.MobileApp;
import org.thingsboard.server.common.data.mobile.MobileAppInfo;
import org.thingsboard.server.common.data.mobile.MobileAppOauth2Client;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.oauth2.OAuth2ClientDao;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.Validator;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@Service
public class MobileAppServiceImpl extends AbstractEntityService implements MobileAppService {
@Autowired
private OAuth2ClientDao oauth2ClientDao;
private static final String PLATFORM_TYPE_IS_REQUIRED = "Platform type is required if package name is specified";
private static final String MOBILE_APP_BUNDLE_CONSTRAINT = "The mobile app referenced by the mobile bundle cannot be deleted!";
@Autowired
private MobileAppDao mobileAppDao;
@Autowired
private DataValidator<MobileApp> mobileAppDataValidator;
@Override
public MobileApp saveMobileApp(TenantId tenantId, MobileApp mobileApp) {
log.trace("Executing saveMobileApp [{}]", mobileApp);
mobileAppDataValidator.validate(mobileApp, a -> tenantId);
try {
MobileApp savedMobileApp = mobileAppDao.save(tenantId, mobileApp);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(savedMobileApp).build());
return savedMobileApp;
} catch (Exception 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;
}
}
@ -69,8 +68,15 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil
@Override
public void deleteMobileAppById(TenantId tenantId, MobileAppId mobileAppId) {
log.trace("Executing deleteMobileAppById [{}]", mobileAppId.getId());
mobileAppDao.removeById(tenantId, mobileAppId.getId());
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(mobileAppId).build());
try {
mobileAppDao.removeById(tenantId, mobileAppId.getId());
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(mobileAppId).build());
} catch (Exception e) {
checkConstraintViolation(e,
Map.of("fk_android_app_id", MOBILE_APP_BUNDLE_CONSTRAINT,
"fk_ios_app_id", MOBILE_APP_BUNDLE_CONSTRAINT));
throw e;
}
}
@Override
@ -80,43 +86,9 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil
}
@Override
public PageData<MobileAppInfo> findMobileAppInfosByTenantId(TenantId tenantId, PageLink pageLink) {
public PageData<MobileApp> findMobileAppsByTenantId(TenantId tenantId, PlatformType platformType, PageLink pageLink) {
log.trace("Executing findMobileAppInfosByTenantId [{}]", tenantId);
PageData<MobileApp> mobiles = mobileAppDao.findByTenantId(tenantId, pageLink);
return mobiles.mapData(this::getMobileAppInfo);
}
@Override
public MobileAppInfo findMobileAppInfoById(TenantId tenantId, MobileAppId mobileAppId) {
log.trace("Executing findMobileAppInfoById [{}] [{}]", tenantId, mobileAppId);
MobileApp mobileApp = mobileAppDao.findById(tenantId, mobileAppId.getId());
if (mobileApp == null) {
return null;
}
return getMobileAppInfo(mobileApp);
}
@Override
public void updateOauth2Clients(TenantId tenantId, MobileAppId mobileAppId, List<OAuth2ClientId> oAuth2ClientIds) {
log.trace("Executing updateOauth2Clients, mobileAppId [{}], oAuth2ClientIds [{}]", mobileAppId, oAuth2ClientIds);
Set<MobileAppOauth2Client> newClientList = oAuth2ClientIds.stream()
.map(clientId -> new MobileAppOauth2Client(mobileAppId, clientId))
.collect(Collectors.toSet());
List<MobileAppOauth2Client> existingClients = mobileAppDao.findOauth2ClientsByMobileAppId(tenantId, mobileAppId);
List<MobileAppOauth2Client> toRemoveList = existingClients.stream()
.filter(client -> !newClientList.contains(client))
.toList();
newClientList.removeIf(existingClients::contains);
for (MobileAppOauth2Client client : toRemoveList) {
mobileAppDao.removeOauth2Client(client);
}
for (MobileAppOauth2Client client : newClientList) {
mobileAppDao.addOauth2Client(client);
}
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId)
.entityId(mobileAppId).created(false).build());
return mobileAppDao.findByTenantId(tenantId, platformType, pageLink);
}
@Override
@ -131,22 +103,22 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil
}
@Override
public void deleteMobileAppsByTenantId(TenantId tenantId) {
log.trace("Executing deleteMobileAppsByTenantId, tenantId [{}]", tenantId);
mobileAppDao.deleteByTenantId(tenantId);
public MobileApp findByBundleIdAndPlatformType(TenantId tenantId, MobileAppBundleId mobileAppBundleId, PlatformType platformType) {
log.trace("Executing findAndroidQrConfig, tenantId [{}], mobileAppBundleId [{}]", tenantId, mobileAppBundleId);
return mobileAppDao.findByBundleIdAndPlatformType(tenantId, mobileAppBundleId, platformType);
}
@Override
public void deleteByTenantId(TenantId tenantId) {
deleteMobileAppsByTenantId(tenantId);
public MobileApp findMobileAppByPkgNameAndPlatformType(String pkgName, PlatformType platformType) {
log.trace("Executing findMobileAppByPkgNameAndPlatformType, pkgName [{}], platform [{}]", pkgName, platformType);
Validator.checkNotNull(platformType, PLATFORM_TYPE_IS_REQUIRED);
return mobileAppDao.findByPkgNameAndPlatformType(TenantId.SYS_TENANT_ID, pkgName, platformType);
}
private MobileAppInfo getMobileAppInfo(MobileApp mobileApp) {
List<OAuth2ClientInfo> clients = oauth2ClientDao.findByMobileAppId(mobileApp.getUuidId()).stream()
.map(OAuth2ClientInfo::new)
.sorted(Comparator.comparing(OAuth2ClientInfo::getTitle))
.collect(Collectors.toList());
return new MobileAppInfo(mobileApp, clients);
@Override
public void deleteByTenantId(TenantId tenantId) {
log.trace("Executing deleteByTenantId, tenantId [{}]", tenantId);
mobileAppDao.deleteByTenantId(tenantId);
}
@Override

12
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java → dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java

@ -16,13 +16,17 @@
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.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings;
import org.thingsboard.server.common.data.oauth2.PlatformType;
public interface MobileAppSettingsService {
public interface QrCodeSettingService {
MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings settings);
QrCodeSettings saveQrCodeSettings(TenantId tenantId, QrCodeSettings qrCodeSettings);
MobileAppSettings getMobileAppSettings(TenantId tenantId);
QrCodeSettings findQrCodeSettings(TenantId tenantId);
MobileApp findAppFromQrCodeSettings(TenantId sysTenantId, PlatformType platformType);
void deleteByTenantId(TenantId tenantId);

134
dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java

@ -0,0 +1,134 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.mobile;
import lombok.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.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.qrCodeSettings.BadgePosition;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings;
import org.thingsboard.server.common.data.mobile.qrCodeSettings.QRCodeConfig;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.service.DataValidator;
import java.util.Map;
import static org.thingsboard.server.common.data.oauth2.PlatformType.ANDROID;
import static org.thingsboard.server.common.data.oauth2.PlatformType.IOS;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service
@Slf4j
@RequiredArgsConstructor
public class QrCodeSettingServiceImpl extends AbstractCachedEntityService<TenantId, QrCodeSettings, QrCodeSettingsEvictEvent> implements QrCodeSettingService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
private static final String DEFAULT_QR_CODE_LABEL = "Scan to connect or download mobile app";
@Value("${mobileApp.googlePlayLink:https://play.google.com/store/apps/details?id=org.thingsboard.demo.app}")
private String googlePlayLink;
@Value("${mobileApp.appStoreLink:https://apps.apple.com/us/app/thingsboard-live/id1594355695}")
private String appStoreLink;
private final QrCodeSettingsDao qrCodeSettingsDao;
private final MobileAppService mobileAppService;
private final DataValidator<QrCodeSettings> mobileAppSettingsDataValidator;
@Override
public QrCodeSettings saveQrCodeSettings(TenantId tenantId, QrCodeSettings qrCodeSettings) {
mobileAppSettingsDataValidator.validate(qrCodeSettings, s -> tenantId);
try {
QrCodeSettings savedQrCodeSettings = qrCodeSettingsDao.save(tenantId, qrCodeSettings);
publishEvictEvent(new QrCodeSettingsEvictEvent(tenantId));
return constructMobileAppSettings(savedQrCodeSettings);
} catch (Exception e) {
handleEvictEvent(new QrCodeSettingsEvictEvent(tenantId));
checkConstraintViolation(e, Map.of(
"qr_code_settings_tenant_id_unq_key", "Mobile application for specified tenant already exists!"
));
throw e;
}
}
@Override
public QrCodeSettings findQrCodeSettings(TenantId tenantId) {
log.trace("Executing getMobileAppSettings for tenant [{}] ", tenantId);
QrCodeSettings qrCodeSettings = cache.getAndPutInTransaction(tenantId,
() -> qrCodeSettingsDao.findByTenantId(tenantId), true);
return constructMobileAppSettings(qrCodeSettings);
}
@Override
public MobileApp findAppFromQrCodeSettings(TenantId tenantId, PlatformType platformType) {
log.trace("Executing findAppQrCodeConfig for tenant [{}] ", tenantId);
QrCodeSettings qrCodeSettings = findQrCodeSettings(tenantId);
return qrCodeSettings.getMobileAppBundleId() != null ? mobileAppService.findByBundleIdAndPlatformType(tenantId, qrCodeSettings.getMobileAppBundleId(), platformType) : null;
}
@Override
public void deleteByTenantId(TenantId tenantId) {
log.trace("Executing deleteByTenantId, tenantId [{}]", tenantId);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
qrCodeSettingsDao.removeByTenantId(tenantId);
}
@TransactionalEventListener(classes = QrCodeSettingsEvictEvent.class)
@Override
public void handleEvictEvent(QrCodeSettingsEvictEvent event) {
cache.evict(event.getTenantId());
}
private QrCodeSettings constructMobileAppSettings(QrCodeSettings qrCodeSettings) {
if (qrCodeSettings == null) {
qrCodeSettings = new QrCodeSettings();
qrCodeSettings.setUseDefaultApp(true);
qrCodeSettings.setAndroidEnabled(true);
qrCodeSettings.setIosEnabled(true);
QRCodeConfig qrCodeConfig = QRCodeConfig.builder()
.showOnHomePage(true)
.qrCodeLabelEnabled(true)
.qrCodeLabel(DEFAULT_QR_CODE_LABEL)
.badgeEnabled(true)
.badgePosition(BadgePosition.RIGHT)
.badgeEnabled(true)
.build();
qrCodeSettings.setQrCodeConfig(qrCodeConfig);
qrCodeSettings.setMobileAppBundleId(qrCodeSettings.getMobileAppBundleId());
}
if (qrCodeSettings.isUseDefaultApp()) {
qrCodeSettings.setGooglePlayLink(googlePlayLink);
qrCodeSettings.setAppStoreLink(appStoreLink);
} else {
MobileApp androidApp = mobileAppService.findByBundleIdAndPlatformType(qrCodeSettings.getTenantId(), qrCodeSettings.getMobileAppBundleId(), ANDROID);
MobileApp iosApp = mobileAppService.findByBundleIdAndPlatformType(qrCodeSettings.getTenantId(), qrCodeSettings.getMobileAppBundleId(), IOS);
if (androidApp != null && androidApp.getStoreInfo() != null) {
qrCodeSettings.setGooglePlayLink(androidApp.getStoreInfo().getStoreLink());
}
if (iosApp != null && iosApp.getStoreInfo() != null) {
qrCodeSettings.setAppStoreLink(iosApp.getStoreInfo().getStoreLink());
}
}
return qrCodeSettings;
}
}

10
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java → dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsCaffeineCache.java

@ -21,14 +21,14 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("MobileAppCache")
public class MobileAppSettingsCaffeineCache extends CaffeineTbTransactionalCache<TenantId, MobileAppSettings> {
@Service("QrCodeSettingsCache")
public class QrCodeSettingsCaffeineCache extends CaffeineTbTransactionalCache<TenantId, QrCodeSettings> {
public MobileAppSettingsCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.MOBILE_APP_SETTINGS_CACHE);
public QrCodeSettingsCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.QR_CODE_SETTINGS_CACHE);
}
}

6
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java → dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsDao.java

@ -16,13 +16,13 @@
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.common.data.mobile.qrCodeSettings.QrCodeSettings;
import org.thingsboard.server.dao.Dao;
public interface MobileAppSettingsDao extends Dao<MobileAppSettings> {
public interface QrCodeSettingsDao extends Dao<QrCodeSettings> {
MobileAppSettings findByTenantId(TenantId tenantId);
QrCodeSettings findByTenantId(TenantId tenantId);
void removeByTenantId(TenantId tenantId);
}

2
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java → dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsEvictEvent.java

@ -19,6 +19,6 @@ import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
@Data
public class MobileAppSettingsEvictEvent {
public class QrCodeSettingsEvictEvent {
private final TenantId tenantId;
}

10
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java → dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsRedisCache.java

@ -24,13 +24,13 @@ import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbJsonRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("MobileAppCache")
public class MobileAppSettingsRedisCache extends RedisTbTransactionalCache<TenantId, MobileAppSettings> {
@Service("QrCodeSettingsCache")
public class QrCodeSettingsRedisCache extends RedisTbTransactionalCache<TenantId, QrCodeSettings> {
public MobileAppSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.MOBILE_APP_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(MobileAppSettings.class));
public QrCodeSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.QR_CODE_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(QrCodeSettings.class));
}
}

33
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -449,11 +449,25 @@ public class ModelConstants {
public static final String MOBILE_APP_TABLE_NAME = "mobile_app";
public static final String MOBILE_APP_PKG_NAME_PROPERTY = "pkg_name";
public static final String MOBILE_APP_APP_SECRET_PROPERTY = "app_secret";
public static final String MOBILE_APP_OAUTH2_ENABLED_PROPERTY = "oauth2_enabled";
public static final String MOBILE_APP_PLATFORM_TYPE_PROPERTY = "platform_type";
public static final String MOBILE_APP_STATUS_PROPERTY = "status";
public static final String MOBILE_APP_VERSION_INFO_PROPERTY = "version_info";
public static final String MOBILE_APP_STORE_INFO_PROPERTY = "store_info";
public static final String MOBILE_APP_OAUTH2_CLIENT_TABLE_NAME = "mobile_app_oauth2_client";
public static final String MOBILE_APP_OAUTH2_CLIENT_CLIENT_ID_PROPERTY = "oauth2_client_id";
public static final String MOBILE_APP_OAUTH2_CLIENT_MOBILE_APP_ID_PROPERTY = "mobile_app_id";
/**
* Mobile application bundle constants.
*/
public static final String MOBILE_APP_BUNDLE_TABLE_NAME = "mobile_app_bundle";
public static final String MOBILE_APP_BUNDLE_TITLE_PROPERTY = "title";
public static final String MOBILE_APP_BUNDLE_DESCRIPTION_PROPERTY = "description";
public static final String MOBILE_APP_BUNDLE_ANDROID_APP_ID_PROPERTY = "android_app_id";
public static final String MOBILE_APP_BUNDLE_IOS_APP_ID_PROPERTY = "ios_app_id";
public static final String MOBILE_APP_BUNDLE_LAYOUT_CONFIG_PROPERTY = "layout_config";
public static final String MOBILE_APP_BUNDLE_OAUTH2_ENABLED_PROPERTY = "oauth2_enabled";
public static final String MOBILE_APP_BUNDLE_OAUTH2_CLIENT_TABLE_NAME = "mobile_app_bundle_oauth2_client";
public static final String MOBILE_APP_BUNDLE_OAUTH2_CLIENT_CLIENT_ID_PROPERTY = "oauth2_client_id";
public static final String MOBILE_APP_BUNDLE_OAUTH2_CLIENT_MOBILE_APP_BUNDLE_ID_PROPERTY = "mobile_app_bundle_id";
/**
@ -685,11 +699,12 @@ public class ModelConstants {
/**
* Mobile application settings constants.
*/
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_SETTINGS_IOS_CONFIG_PROPERTY = "ios_config";
public static final String MOBILE_APP_SETTINGS_QR_CODE_CONFIG_PROPERTY = "qr_code_config";
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_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_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};

114
dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractMobileAppBundleEntity.java

@ -0,0 +1,114 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.MappedSuperclass;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.mapping.JsonConverter;
import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ID_COLUMN;
@Data
@EqualsAndHashCode(callSuper = true)
@MappedSuperclass
public abstract class AbstractMobileAppBundleEntity<T extends MobileAppBundle> extends BaseSqlEntity<T> {
@Column(name = TENANT_ID_COLUMN)
private UUID tenantId;
@Column(name = ModelConstants.MOBILE_APP_BUNDLE_TITLE_PROPERTY)
private String title;
@Column(name = ModelConstants.MOBILE_APP_BUNDLE_DESCRIPTION_PROPERTY)
private String description;
@Column(name = ModelConstants.MOBILE_APP_BUNDLE_ANDROID_APP_ID_PROPERTY)
private UUID androidAppId;
@Column(name = ModelConstants.MOBILE_APP_BUNDLE_IOS_APP_ID_PROPERTY)
private UUID iosAppID;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.MOBILE_APP_BUNDLE_LAYOUT_CONFIG_PROPERTY)
private JsonNode layoutConfig;
@Column(name = ModelConstants.MOBILE_APP_BUNDLE_OAUTH2_ENABLED_PROPERTY)
private Boolean oauth2Enabled;
public AbstractMobileAppBundleEntity() {
super();
}
public AbstractMobileAppBundleEntity(MobileAppBundleEntity mobileAppBundleEntity) {
super(mobileAppBundleEntity);
this.tenantId = mobileAppBundleEntity.getTenantId();
this.title = mobileAppBundleEntity.getTitle();
this.description = mobileAppBundleEntity.getDescription();
this.androidAppId = mobileAppBundleEntity.getAndroidAppId();
this.iosAppID = mobileAppBundleEntity.getIosAppID();
this.layoutConfig = mobileAppBundleEntity.getLayoutConfig();
this.oauth2Enabled = mobileAppBundleEntity.getOauth2Enabled();
}
public AbstractMobileAppBundleEntity(T mobileAppBundle) {
super(mobileAppBundle);
if (mobileAppBundle.getTenantId() != null) {
this.tenantId = mobileAppBundle.getTenantId().getId();
}
this.title = mobileAppBundle.getTitle();
this.description = mobileAppBundle.getDescription();
if (mobileAppBundle.getAndroidAppId() != null) {
this.androidAppId = mobileAppBundle.getAndroidAppId().getId();
}
if (mobileAppBundle.getIosAppId() != null) {
this.iosAppID = mobileAppBundle.getIosAppId().getId();
}
this.layoutConfig = toJson(mobileAppBundle.getLayoutConfig());
this.oauth2Enabled = mobileAppBundle.getOauth2Enabled();
}
protected MobileAppBundle toMobileAppBundle() {
MobileAppBundle mobileAppBundle = new MobileAppBundle(new MobileAppBundleId(id));
mobileAppBundle.setCreatedTime(createdTime);
mobileAppBundle.setTitle(title);
mobileAppBundle.setDescription(description);
if (tenantId != null) {
mobileAppBundle.setTenantId(TenantId.fromUUID(tenantId));
}
if (androidAppId != null) {
mobileAppBundle.setAndroidAppId(new MobileAppId(androidAppId));
}
if (iosAppID != null) {
mobileAppBundle.setIosAppId(new MobileAppId(iosAppID));
}
mobileAppBundle.setLayoutConfig(fromJson(layoutConfig, MobileLayoutConfig.class));
mobileAppBundle.setOauth2Enabled(oauth2Enabled);
return mobileAppBundle;
}
}

35
common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppInfo.java → dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java

@ -13,35 +13,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.mobile;
package org.thingsboard.server.dao.model.sql;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import java.util.List;
import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_TABLE_NAME;
@EqualsAndHashCode(callSuper = true)
@Data
@Schema
public class MobileAppInfo extends MobileApp {
@Schema(description = "List of available oauth2 clients")
private List<OAuth2ClientInfo> oauth2ClientInfos;
public MobileAppInfo(MobileApp mobileApp, List<OAuth2ClientInfo> oauth2ClientInfos) {
super(mobileApp);
this.oauth2ClientInfos = oauth2ClientInfos;
}
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = MOBILE_APP_BUNDLE_TABLE_NAME)
public final class MobileAppBundleEntity extends AbstractMobileAppBundleEntity<MobileAppBundle> {
public MobileAppInfo() {
public MobileAppBundleEntity() {
super();
}
public MobileAppInfo(MobileAppId mobileAppId) {
super(mobileAppId);
public MobileAppBundleEntity(MobileAppBundle mobileAppBundle) {
super(mobileAppBundle);
}
@Override
public MobileAppBundle toData() {
return super.toMobileAppBundle();
}
}

45
dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleInfoEntity.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.model.sql;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo;
@Data
@EqualsAndHashCode(callSuper = true)
public class MobileAppBundleInfoEntity extends AbstractMobileAppBundleEntity<MobileAppBundleInfo> {
private String androidPkgName;
private String iosPkgName;
private boolean qrCodeEnabled;
public MobileAppBundleInfoEntity() {
super();
}
public MobileAppBundleInfoEntity(MobileAppBundleEntity mobileAppBundleEntity, String androidPkgName, String iosPkgName, boolean qrCodeEnabled) {
super(mobileAppBundleEntity);
this.androidPkgName = androidPkgName;
this.iosPkgName = iosPkgName;
this.qrCodeEnabled = qrCodeEnabled;
}
@Override
public MobileAppBundleInfo toData() {
return new MobileAppBundleInfo(super.toMobileAppBundle(), androidPkgName, iosPkgName, qrCodeEnabled);
}
}

35
dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientEntity.java → dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleOauth2ClientEntity.java

@ -21,44 +21,45 @@ import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.Table;
import lombok.Data;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.mobile.MobileAppOauth2Client;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleOauth2Client;
import org.thingsboard.server.dao.model.ToData;
import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_OAUTH2_CLIENT_MOBILE_APP_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_OAUTH2_CLIENT_CLIENT_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_OAUTH2_CLIENT_TABLE_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_OAUTH2_CLIENT_CLIENT_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_OAUTH2_CLIENT_MOBILE_APP_BUNDLE_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_OAUTH2_CLIENT_TABLE_NAME;
@Data
@Entity
@Table(name = MOBILE_APP_OAUTH2_CLIENT_TABLE_NAME)
@Table(name = MOBILE_APP_BUNDLE_OAUTH2_CLIENT_TABLE_NAME)
@IdClass(MobileAppOauth2ClientCompositeKey.class)
public final class MobileAppOauth2ClientEntity implements ToData<MobileAppOauth2Client> {
public final class MobileAppBundleOauth2ClientEntity implements ToData<MobileAppBundleOauth2Client> {
@Id
@Column(name = MOBILE_APP_OAUTH2_CLIENT_MOBILE_APP_ID_PROPERTY, columnDefinition = "uuid")
private UUID mobileAppId;
@Column(name = MOBILE_APP_BUNDLE_OAUTH2_CLIENT_MOBILE_APP_BUNDLE_ID_PROPERTY, columnDefinition = "uuid")
private UUID mobileAppBundleId;
@Id
@Column(name = MOBILE_APP_OAUTH2_CLIENT_CLIENT_ID_PROPERTY, columnDefinition = "uuid")
@Column(name = MOBILE_APP_BUNDLE_OAUTH2_CLIENT_CLIENT_ID_PROPERTY, columnDefinition = "uuid")
private UUID oauth2ClientId;
public MobileAppOauth2ClientEntity() {
public MobileAppBundleOauth2ClientEntity() {
super();
}
public MobileAppOauth2ClientEntity(MobileAppOauth2Client domainOauth2Provider) {
mobileAppId = domainOauth2Provider.getMobileAppId().getId();
oauth2ClientId = domainOauth2Provider.getOAuth2ClientId().getId();
public MobileAppBundleOauth2ClientEntity(MobileAppBundleOauth2Client mobileAppBundleOauth2Client) {
mobileAppBundleId = mobileAppBundleOauth2Client.getMobileAppBundleId().getId();
oauth2ClientId = mobileAppBundleOauth2Client.getOAuth2ClientId().getId();
}
@Override
public MobileAppOauth2Client toData() {
MobileAppOauth2Client result = new MobileAppOauth2Client();
result.setMobileAppId(new MobileAppId(mobileAppId));
public MobileAppBundleOauth2Client toData() {
MobileAppBundleOauth2Client result = new MobileAppBundleOauth2Client();
result.setMobileAppBundleId(new MobileAppBundleId(mobileAppBundleId));
result.setOAuth2ClientId(new OAuth2ClientId(oauth2ClientId));
return result;
}

38
dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java

@ -15,16 +15,25 @@
*/
package org.thingsboard.server.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileApp;
import org.thingsboard.server.common.data.mobile.app.MobileAppStatus;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.mobile.app.MobileAppVersionInfo;
import org.thingsboard.server.common.data.mobile.app.StoreInfo;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.mapping.JsonConverter;
import java.util.UUID;
@ -45,8 +54,21 @@ public class MobileAppEntity extends BaseSqlEntity<MobileApp> {
@Column(name = ModelConstants.MOBILE_APP_APP_SECRET_PROPERTY)
private String appSecret;
@Column(name = ModelConstants.MOBILE_APP_OAUTH2_ENABLED_PROPERTY)
private Boolean oauth2Enabled;
@Enumerated(EnumType.STRING)
@Column(name = ModelConstants.MOBILE_APP_PLATFORM_TYPE_PROPERTY)
private PlatformType platformType;
@Enumerated(EnumType.STRING)
@Column(name = ModelConstants.MOBILE_APP_STATUS_PROPERTY)
private MobileAppStatus status;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.MOBILE_APP_VERSION_INFO_PROPERTY)
private JsonNode versionInfo;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.MOBILE_APP_STORE_INFO_PROPERTY)
private JsonNode storeInfo;
public MobileAppEntity() {
super();
@ -59,7 +81,10 @@ public class MobileAppEntity extends BaseSqlEntity<MobileApp> {
}
this.pkgName = mobile.getPkgName();
this.appSecret = mobile.getAppSecret();
this.oauth2Enabled = mobile.isOauth2Enabled();
this.platformType = mobile.getPlatformType();
this.status = mobile.getStatus();
this.versionInfo = toJson(mobile.getVersionInfo());
this.storeInfo = toJson(mobile.getStoreInfo());
}
@Override
@ -72,7 +97,10 @@ public class MobileAppEntity extends BaseSqlEntity<MobileApp> {
mobile.setCreatedTime(createdTime);
mobile.setPkgName(pkgName);
mobile.setAppSecret(appSecret);
mobile.setOauth2Enabled(oauth2Enabled);
mobile.setPlatformType(platformType);
mobile.setStatus(status);
mobile.setVersionInfo(fromJson(versionInfo, MobileAppVersionInfo.class));
mobile.setStoreInfo(fromJson(storeInfo, StoreInfo.class));
return mobile;
}
}

2
dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientCompositeKey.java

@ -31,7 +31,7 @@ public class MobileAppOauth2ClientCompositeKey implements Serializable {
@Transient
private static final long serialVersionUID = -245388185894468455L;
private UUID mobileAppId;
private UUID mobileAppBundleId;
private UUID oauth2ClientId;
}

85
dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java

@ -1,85 +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.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
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.util.mapping.JsonConverter;
import java.util.UUID;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@Entity
@Table(name = ModelConstants.MOBILE_APP_SETTINGS_TABLE_NAME)
public class MobileAppSettingsEntity extends BaseSqlEntity<MobileAppSettings> {
@Column(name = ModelConstants.TENANT_ID_COLUMN, columnDefinition = "uuid")
protected UUID tenantId;
@Column(name = ModelConstants.MOBILE_APP_SETTINGS_USE_DEFAULT_APP_PROPERTY)
private boolean useDefaultApp;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.MOBILE_APP_SETTINGS_ANDROID_CONFIG_PROPERTY)
private JsonNode androidConfig;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.MOBILE_APP_SETTINGS_IOS_CONFIG_PROPERTY)
private JsonNode iosConfig;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.MOBILE_APP_SETTINGS_QR_CODE_CONFIG_PROPERTY)
private JsonNode qrCodeConfig;
public MobileAppSettingsEntity(MobileAppSettings mobileAppSettings) {
this.setId(mobileAppSettings.getUuidId());
this.setCreatedTime(mobileAppSettings.getCreatedTime());
this.tenantId = mobileAppSettings.getTenantId().getId();
this.useDefaultApp = mobileAppSettings.isUseDefaultApp();
this.androidConfig = toJson(mobileAppSettings.getAndroidConfig());
this.iosConfig = toJson(mobileAppSettings.getIosConfig());
this.qrCodeConfig = toJson(mobileAppSettings.getQrCodeConfig());
}
@Override
public MobileAppSettings toData() {
MobileAppSettings mobileAppSettings = new MobileAppSettings(new MobileAppSettingsId(getUuid()));
mobileAppSettings.setCreatedTime(createdTime);
mobileAppSettings.setTenantId(TenantId.fromUUID(tenantId));
mobileAppSettings.setUseDefaultApp(useDefaultApp);
mobileAppSettings.setAndroidConfig(fromJson(androidConfig, AndroidConfig.class));
mobileAppSettings.setIosConfig(fromJson(iosConfig, IosConfig.class));
mobileAppSettings.setQrCodeConfig(fromJson(qrCodeConfig, QRCodeConfig.class));
return mobileAppSettings;
}
}

91
dao/src/main/java/org/thingsboard/server/dao/model/sql/QrCodeSettingsEntity.java

@ -0,0 +1,91 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.QrCodeSettingsId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings;
import org.thingsboard.server.common.data.mobile.qrCodeSettings.QRCodeConfig;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.mapping.JsonConverter;
import java.util.UUID;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@Entity
@Table(name = ModelConstants.QR_CODE_SETTINGS_TABLE_NAME)
public class QrCodeSettingsEntity extends BaseSqlEntity<QrCodeSettings> {
@Column(name = ModelConstants.TENANT_ID_COLUMN, columnDefinition = "uuid")
protected UUID tenantId;
@Column(name = ModelConstants.QR_CODE_SETTINGS_USE_DEFAULT_APP_PROPERTY)
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)
private UUID mobileAppBundleId;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.QR_CODE_SETTINGS_CONFIG_PROPERTY)
private JsonNode qrCodeConfig;
public QrCodeSettingsEntity(QrCodeSettings qrCodeSettings) {
this.setId(qrCodeSettings.getUuidId());
this.setCreatedTime(qrCodeSettings.getCreatedTime());
this.tenantId = qrCodeSettings.getTenantId().getId();
this.useDefaultApp = qrCodeSettings.isUseDefaultApp();
this.androidEnabled = qrCodeSettings.isAndroidEnabled();
this.iosEnabled = qrCodeSettings.isIosEnabled();
if (qrCodeSettings.getMobileAppBundleId() != null) {
this.mobileAppBundleId = qrCodeSettings.getMobileAppBundleId().getId();
}
this.qrCodeConfig = toJson(qrCodeSettings.getQrCodeConfig());
}
@Override
public QrCodeSettings toData() {
QrCodeSettings qrCodeSettings = new QrCodeSettings(new QrCodeSettingsId(getUuid()));
qrCodeSettings.setCreatedTime(createdTime);
qrCodeSettings.setTenantId(TenantId.fromUUID(tenantId));
qrCodeSettings.setUseDefaultApp(useDefaultApp);
qrCodeSettings.setAndroidEnabled(androidEnabled);
qrCodeSettings.setIosEnabled(iosEnabled);
if (mobileAppBundleId != null) {
qrCodeSettings.setMobileAppBundleId(new MobileAppBundleId(mobileAppBundleId));
}
qrCodeSettings.setQrCodeConfig(fromJson(qrCodeConfig, QRCodeConfig.class));
return qrCodeSettings;
}
}

2
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java

@ -36,7 +36,7 @@ public interface OAuth2ClientDao extends Dao<OAuth2Client> {
List<OAuth2Client> findByDomainId(UUID domainId);
List<OAuth2Client> findByMobileAppId(UUID mobileAppId);
List<OAuth2Client> findByMobileAppBundleId(UUID mobileAppBundleId);
String findAppSecret(UUID id, String pkgName);

60
dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppBundleDataValidator.java

@ -0,0 +1,60 @@
/**
* 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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.mobile.MobileAppDao;
import org.thingsboard.server.dao.service.DataValidator;
@Component
@AllArgsConstructor
public class MobileAppBundleDataValidator extends DataValidator<MobileAppBundle> {
@Autowired
private MobileAppDao mobileAppDao;
@Override
protected void validateDataImpl(TenantId tenantId, MobileAppBundle mobileAppBundle) {
MobileAppId androidAppId = mobileAppBundle.getAndroidAppId();
if (androidAppId != null) {
MobileApp androidApp = mobileAppDao.findById(tenantId, androidAppId.getId());
if (androidApp == null) {
throw new DataValidationException("Mobile app bundle refers to non-existing android app!");
}
if (androidApp.getPlatformType() != PlatformType.ANDROID) {
throw new DataValidationException("Mobile app bundle refers to wrong android app! Platform type of specified app is " + androidApp.getPlatformType());
}
}
MobileAppId iosAppId = mobileAppBundle.getIosAppId();
if (iosAppId != null) {
MobileApp iosApp = mobileAppDao.findById(tenantId, iosAppId.getId());
if (iosApp == null) {
throw new DataValidationException("Mobile app bundle refers to non-existing ios app!");
}
if (iosApp.getPlatformType() != PlatformType.IOS) {
throw new DataValidationException("Mobile app bundle refers to wrong ios app! Platform type of specified app is " + iosApp.getPlatformType());
}
}
}
}

50
dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppDataValidator.java

@ -0,0 +1,50 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.app.MobileApp;
import org.thingsboard.server.common.data.mobile.app.MobileAppStatus;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
@Component
@AllArgsConstructor
public class MobileAppDataValidator extends DataValidator<MobileApp> {
@Override
protected void validateDataImpl(TenantId tenantId, MobileApp mobileApp) {
if (mobileApp.getPlatformType() == PlatformType.ANDROID) {
if (mobileApp.getStoreInfo() != null &&
(mobileApp.getStoreInfo().getSha256CertFingerprints() == null || mobileApp.getStoreInfo().getStoreLink() == null)) {
throw new DataValidationException("Sha256CertFingerprints and store link are required");
}
} else if (mobileApp.getPlatformType() == PlatformType.IOS) {
if (mobileApp.getStoreInfo() != null &&
(mobileApp.getStoreInfo().getAppId() == null || mobileApp.getStoreInfo().getStoreLink() == null)) {
throw new DataValidationException("AppId and store link are required");
}
} else {
throw new DataValidationException("Wrong application platform type");
}
if (mobileApp.getStatus() == MobileAppStatus.PUBLISHED && mobileApp.getStoreInfo() == null) {
throw new DataValidationException("Store info is required for published apps");
}
}
}

51
dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java

@ -1,51 +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.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<MobileAppSettings> {
@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 configuration is required!");
}
if (androidConfig != null && androidConfig.isEnabled() && !mobileAppSettings.isUseDefaultApp() &&
(androidConfig.getAppPackage() == null || androidConfig.getSha256CertFingerprints() == null)) {
throw new DataValidationException("Application package and sha256 cert fingerprints are required for custom android application!");
}
if (iosConfig != null && iosConfig.isEnabled() && !mobileAppSettings.isUseDefaultApp() && iosConfig.getAppId() == null) {
throw new DataValidationException("Application id is required for custom ios application!");
}
}
}

59
dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java

@ -0,0 +1,59 @@
/**
* 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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.TenantId;
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.qrCodeSettings.QrCodeSettings;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.mobile.MobileAppDao;
import org.thingsboard.server.dao.service.DataValidator;
@Component
@AllArgsConstructor
public class QrCodeSettingsDataValidator extends DataValidator<QrCodeSettings> {
@Autowired
MobileAppDao mobileAppDao;
@Override
protected void validateDataImpl(TenantId tenantId, QrCodeSettings qrCodeSettings) {
MobileAppBundleId mobileAppBundleId = qrCodeSettings.getMobileAppBundleId();
if (!qrCodeSettings.isUseDefaultApp() && (mobileAppBundleId == null)) {
throw new DataValidationException("Mobile app bundle is required to use custom application!");
}
if (!qrCodeSettings.isUseDefaultApp()) {
if (qrCodeSettings.isAndroidEnabled()) {
MobileApp androidApp = mobileAppDao.findByBundleIdAndPlatformType(tenantId, mobileAppBundleId, PlatformType.ANDROID);
if (androidApp != null && androidApp.getStatus() != MobileAppStatus.PUBLISHED) {
throw new DataValidationException("The mobile app bundle references an Android app that has not been published!");
}
}
if (qrCodeSettings.isIosEnabled()) {
MobileApp iosApp = mobileAppDao.findByBundleIdAndPlatformType(tenantId, mobileAppBundleId, PlatformType.IOS);
if (iosApp != null && iosApp.getStatus() != MobileAppStatus.PUBLISHED) {
throw new DataValidationException("The mobile app bundle references an iOS app that has not been published!");
}
}
}
}
}

101
dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java

@ -0,0 +1,101 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.mobile;
import lombok.RequiredArgsConstructor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleOauth2Client;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.mobile.MobileAppBundleDao;
import org.thingsboard.server.dao.model.sql.MobileAppBundleEntity;
import org.thingsboard.server.dao.model.sql.MobileAppBundleOauth2ClientEntity;
import org.thingsboard.server.dao.model.sql.MobileAppOauth2ClientCompositeKey;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
import java.util.UUID;
@Component
@RequiredArgsConstructor
@SqlDao
public class JpaMobileAppBundleDao extends JpaAbstractDao<MobileAppBundleEntity, MobileAppBundle> implements MobileAppBundleDao {
private final MobileAppBundleRepository mobileAppBundleRepository;
private final MobileAppBundleOauth2ClientRepository mobileOauth2ProviderRepository;
@Override
protected Class<MobileAppBundleEntity> getEntityClass() {
return MobileAppBundleEntity.class;
}
@Override
protected JpaRepository<MobileAppBundleEntity, UUID> getRepository() {
return mobileAppBundleRepository;
}
@Override
public PageData<MobileAppBundleInfo> findInfosByTenantId(TenantId tenantId, PageLink pageLink) {
return DaoUtil.toPageData(mobileAppBundleRepository.findInfoByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink)));
}
@Override
public MobileAppBundleInfo findInfoById(TenantId tenantId, MobileAppBundleId mobileAppBundleId) {
return DaoUtil.getData(mobileAppBundleRepository.findInfoById(mobileAppBundleId.getId()));
}
@Override
public List<MobileAppBundleOauth2Client> findOauth2ClientsByMobileAppBundleId(TenantId tenantId, MobileAppBundleId mobileAppBundleId) {
return DaoUtil.convertDataList(mobileOauth2ProviderRepository.findAllByMobileAppBundleId(mobileAppBundleId.getId()));
}
@Override
public void addOauth2Client(TenantId tenantId, MobileAppBundleOauth2Client mobileAppBundleOauth2Client) {
mobileOauth2ProviderRepository.save(new MobileAppBundleOauth2ClientEntity(mobileAppBundleOauth2Client));
}
@Override
public void removeOauth2Client(TenantId tenantId, MobileAppBundleOauth2Client mobileAppBundleOauth2Client) {
mobileOauth2ProviderRepository.deleteById(new MobileAppOauth2ClientCompositeKey(mobileAppBundleOauth2Client.getMobileAppBundleId().getId(),
mobileAppBundleOauth2Client.getOAuth2ClientId().getId()));
}
@Override
public MobileAppBundle findByPkgNameAndPlatform(TenantId tenantId, String pkgName, PlatformType platform) {
return DaoUtil.getData(mobileAppBundleRepository.findByPkgNameAndPlatformType(pkgName, platform));
}
@Override
public void deleteByTenantId(TenantId tenantId) {
mobileAppBundleRepository.deleteByTenantId(tenantId.getId());
}
@Override
public EntityType getEntityType() {
return EntityType.MOBILE_APP_BUNDLE;
}
}

36
dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java

@ -19,21 +19,18 @@ import lombok.RequiredArgsConstructor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileApp;
import org.thingsboard.server.common.data.mobile.MobileAppOauth2Client;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.mobile.MobileAppDao;
import org.thingsboard.server.dao.model.sql.MobileAppEntity;
import org.thingsboard.server.dao.model.sql.MobileAppOauth2ClientCompositeKey;
import org.thingsboard.server.dao.model.sql.MobileAppOauth2ClientEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
import java.util.UUID;
@Component
@ -42,7 +39,6 @@ import java.util.UUID;
public class JpaMobileAppDao extends JpaAbstractDao<MobileAppEntity, MobileApp> implements MobileAppDao {
private final MobileAppRepository mobileAppRepository;
private final MobileAppOauth2ClientRepository mobileOauth2ProviderRepository;
@Override
protected Class<MobileAppEntity> getEntityClass() {
@ -55,29 +51,27 @@ public class JpaMobileAppDao extends JpaAbstractDao<MobileAppEntity, MobileApp>
}
@Override
public PageData<MobileApp> findByTenantId(TenantId tenantId, PageLink pageLink) {
return DaoUtil.toPageData(mobileAppRepository.findByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink)));
public MobileApp findByBundleIdAndPlatformType(TenantId tenantId, MobileAppBundleId mobileAppBundleId, PlatformType platformType) {
return switch (platformType) {
case ANDROID -> DaoUtil.getData(mobileAppRepository.findAndroidAppByBundleId(mobileAppBundleId.getId()));
case IOS -> DaoUtil.getData(mobileAppRepository.findIOSAppByBundleId(mobileAppBundleId.getId()));
default -> null;
};
}
@Override
public List<MobileAppOauth2Client> findOauth2ClientsByMobileAppId(TenantId tenantId, MobileAppId mobileAppId) {
return DaoUtil.convertDataList(mobileOauth2ProviderRepository.findAllByMobileAppId(mobileAppId.getId()));
public PageData<MobileApp> findByTenantId(TenantId tenantId, PlatformType platformType, PageLink pageLink) {
return DaoUtil.toPageData(mobileAppRepository.findByTenantId(tenantId.getId(), platformType, pageLink.getTextSearch(), DaoUtil.toPageable(pageLink)));
}
@Override
public void addOauth2Client(MobileAppOauth2Client mobileAppOauth2Client) {
mobileOauth2ProviderRepository.save(new MobileAppOauth2ClientEntity(mobileAppOauth2Client));
}
@Override
public void removeOauth2Client(MobileAppOauth2Client mobileAppOauth2Client) {
mobileOauth2ProviderRepository.deleteById(new MobileAppOauth2ClientCompositeKey(mobileAppOauth2Client.getMobileAppId().getId(),
mobileAppOauth2Client.getOAuth2ClientId().getId()));
public void deleteByTenantId(TenantId tenantId) {
mobileAppRepository.deleteByTenantId(tenantId.getId());
}
@Override
public void deleteByTenantId(TenantId tenantId) {
mobileAppRepository.deleteByTenantId(tenantId.getId());
public MobileApp findByPkgNameAndPlatformType(TenantId tenantId, String pkgName, PlatformType platform) {
return DaoUtil.getData(mobileAppRepository.findByPkgNameAndPlatformType(pkgName, platform));
}
@Override

24
dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppSettingsDao.java → dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaQrCodeSettingsDao.java

@ -20,10 +20,10 @@ 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.common.data.mobile.qrCodeSettings.QrCodeSettings;
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.mobile.QrCodeSettingsDao;
import org.thingsboard.server.dao.model.sql.QrCodeSettingsEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
@ -33,29 +33,29 @@ import java.util.UUID;
@Component
@Slf4j
@SqlDao
public class JpaMobileAppSettingsDao extends JpaAbstractDao<MobileAppSettingsEntity, MobileAppSettings> implements MobileAppSettingsDao {
public class JpaQrCodeSettingsDao extends JpaAbstractDao<QrCodeSettingsEntity, QrCodeSettings> implements QrCodeSettingsDao {
@Autowired
private MobileAppSettingsRepository mobileAppSettingsRepository;
private QrCodeSettingsRepository qrCodeSettingsRepository;
@Override
public MobileAppSettings findByTenantId(TenantId tenantId) {
return DaoUtil.getData(mobileAppSettingsRepository.findByTenantId(tenantId.getId()));
public QrCodeSettings findByTenantId(TenantId tenantId) {
return DaoUtil.getData(qrCodeSettingsRepository.findByTenantId(tenantId.getId()));
}
@Override
public void removeByTenantId(TenantId tenantId) {
mobileAppSettingsRepository.deleteByTenantId(tenantId.getId());
qrCodeSettingsRepository.deleteByTenantId(tenantId.getId());
}
@Override
protected Class<MobileAppSettingsEntity> getEntityClass() {
return MobileAppSettingsEntity.class;
protected Class<QrCodeSettingsEntity> getEntityClass() {
return QrCodeSettingsEntity.class;
}
@Override
protected JpaRepository<MobileAppSettingsEntity, UUID> getRepository() {
return mobileAppSettingsRepository;
protected JpaRepository<QrCodeSettingsEntity, UUID> getRepository() {
return qrCodeSettingsRepository;
}
}

6
dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppOauth2ClientRepository.java → dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleOauth2ClientRepository.java

@ -17,13 +17,13 @@ package org.thingsboard.server.dao.sql.mobile;
import org.springframework.data.jpa.repository.JpaRepository;
import org.thingsboard.server.dao.model.sql.MobileAppOauth2ClientCompositeKey;
import org.thingsboard.server.dao.model.sql.MobileAppOauth2ClientEntity;
import org.thingsboard.server.dao.model.sql.MobileAppBundleOauth2ClientEntity;
import java.util.List;
import java.util.UUID;
public interface MobileAppOauth2ClientRepository extends JpaRepository<MobileAppOauth2ClientEntity, MobileAppOauth2ClientCompositeKey> {
public interface MobileAppBundleOauth2ClientRepository extends JpaRepository<MobileAppBundleOauth2ClientEntity, MobileAppOauth2ClientCompositeKey> {
List<MobileAppOauth2ClientEntity> findAllByMobileAppId(UUID mobileAppId);
List<MobileAppBundleOauth2ClientEntity> findAllByMobileAppBundleId(UUID mobileAppId);
}

70
dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java

@ -0,0 +1,70 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.mobile;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.dao.model.sql.MobileAppBundleEntity;
import org.thingsboard.server.dao.model.sql.MobileAppBundleInfoEntity;
import java.util.UUID;
public interface MobileAppBundleRepository extends JpaRepository<MobileAppBundleEntity, UUID> {
@Query("SELECT new org.thingsboard.server.dao.model.sql.MobileAppBundleInfoEntity(b, andApp.pkgName, iosApp.pkgName, " +
"((andApp.status IS NOT NULL AND andApp.status = 'PUBLISHED') OR (iosApp.status IS NOT NULL AND iosApp.status = 'PUBLISHED'))) " +
"FROM MobileAppBundleEntity b " +
"LEFT JOIN MobileAppEntity andApp ON b.androidAppId = andApp.id " +
"LEFT JOIN MobileAppEntity iosApp ON b.iosAppID = iosApp.id " +
"WHERE b.tenantId = :tenantId AND " +
"(:searchText is NULL OR ilike(b.title, concat('%', :searchText, '%')) = true)")
Page<MobileAppBundleInfoEntity> findInfoByTenantId(@Param("tenantId") UUID tenantId,
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.MobileAppBundleInfoEntity(b, andApp.pkgName, iosApp.pkgName, " +
"((andApp.status IS NOT NULL AND andApp.status = 'PUBLISHED') OR (iosApp.status IS NOT NULL AND iosApp.status = 'PUBLISHED'))) " +
"FROM MobileAppBundleEntity b " +
"LEFT JOIN MobileAppEntity andApp on b.androidAppId = andApp.id " +
"LEFT JOIN MobileAppEntity iosApp on b.iosAppID = iosApp.id " +
"WHERE b.id = :bundleId ")
MobileAppBundleInfoEntity findInfoById(UUID bundleId);
@Query("SELECT b " +
"FROM MobileAppBundleEntity b " +
"LEFT JOIN MobileAppEntity a ON b.androidAppId = a.id OR b.iosAppID = a.id " +
"WHERE a.pkgName = :pkgName AND a.platformType = :platformType")
MobileAppBundleEntity findByPkgNameAndPlatformType(@Param("pkgName") String pkgName,
@Param("platformType") PlatformType platformType);
@Query("SELECT b FROM MobileAppBundleEntity b WHERE b.tenantId = :tenantId AND " +
"(:searchText is NULL OR ilike(b.title, concat('%', :searchText, '%')) = true)")
Page<MobileAppBundleEntity> findByTenantId(@Param("tenantId") UUID tenantId,
@Param("searchText") String searchText,
Pageable pageable);
@Transactional
@Modifying
@Query("DELETE FROM MobileAppBundleEntity r WHERE r.tenantId = :tenantId")
void deleteByTenantId(@Param("tenantId") UUID tenantId);
}

11
dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppRepository.java

@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.dao.model.sql.MobileAppEntity;
import java.util.UUID;
@ -29,14 +30,24 @@ import java.util.UUID;
public interface MobileAppRepository extends JpaRepository<MobileAppEntity, UUID> {
@Query("SELECT a FROM MobileAppEntity a WHERE a.tenantId = :tenantId AND " +
"(:platformType is NULL OR a.platformType = :platformType) AND" +
"(:searchText is NULL OR ilike(a.pkgName, concat('%', :searchText, '%')) = true)")
Page<MobileAppEntity> findByTenantId(@Param("tenantId") UUID tenantId,
@Param("platformType") PlatformType platformType,
@Param("searchText") String searchText,
Pageable pageable);
MobileAppEntity findByPkgNameAndPlatformType(@Param("pkgName") String pkgName, @Param("platformType") PlatformType platformType);
@Transactional
@Modifying
@Query("DELETE FROM MobileAppEntity r WHERE r.tenantId = :tenantId")
void deleteByTenantId(@Param("tenantId") UUID tenantId);
@Query("SELECT a FROM MobileAppEntity a LEFT JOIN MobileAppBundleEntity b ON b.androidAppId = a.id WHERE b.id = :bundleId")
MobileAppEntity findAndroidAppByBundleId(@Param("bundleId") UUID bundleId);
@Query("SELECT a FROM MobileAppEntity a LEFT JOIN MobileAppBundleEntity b ON b.iosAppID = a.id WHERE b.id = :bundleId")
MobileAppEntity findIOSAppByBundleId(@Param("bundleId") UUID bundleId);
}

8
dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java → dao/src/main/java/org/thingsboard/server/dao/sql/mobile/QrCodeSettingsRepository.java

@ -20,17 +20,17 @@ import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sql.MobileAppSettingsEntity;
import org.thingsboard.server.dao.model.sql.QrCodeSettingsEntity;
import java.util.UUID;
public interface MobileAppSettingsRepository extends JpaRepository<MobileAppSettingsEntity, UUID> {
public interface QrCodeSettingsRepository extends JpaRepository<QrCodeSettingsEntity, UUID> {
MobileAppSettingsEntity findByTenantId(@Param("tenantId") UUID tenantId);
QrCodeSettingsEntity findByTenantId(@Param("tenantId") UUID tenantId);
@Transactional
@Modifying
@Query("DELETE FROM MobileAppSettingsEntity r WHERE r.tenantId = :tenantId")
@Query("DELETE FROM QrCodeSettingsEntity r WHERE r.tenantId = :tenantId")
void deleteByTenantId(@Param("tenantId") UUID tenantId);
}

4
dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java

@ -75,8 +75,8 @@ public class JpaOAuth2ClientDao extends JpaAbstractDao<OAuth2ClientEntity, OAuth
}
@Override
public List<OAuth2Client> findByMobileAppId(UUID mobileAppId) {
return DaoUtil.convertDataList(repository.findByMobileAppId(mobileAppId));
public List<OAuth2Client> findByMobileAppBundleId(UUID mobileAppBundleId) {
return DaoUtil.convertDataList(repository.findByMobileAppBundleId(mobileAppBundleId));
}
@Override

32
dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java

@ -37,8 +37,8 @@ public interface OAuth2ClientRepository extends JpaRepository<OAuth2ClientEntity
@Query("SELECT c " +
"FROM OAuth2ClientEntity c " +
"LEFT JOIN DomainOauth2ClientEntity dc on c.id = dc.oauth2ClientId " +
"LEFT JOIN DomainEntity domain on dc.domainId = domain.id " +
"LEFT JOIN DomainOauth2ClientEntity dc ON c.id = dc.oauth2ClientId " +
"LEFT JOIN DomainEntity domain ON dc.domainId = domain.id " +
"WHERE domain.name = :domainName AND domain.oauth2Enabled = true " +
"AND (:platformFilter IS NULL OR c.platforms IS NULL OR c.platforms = '' OR ilike(c.platforms, CONCAT('%', :platformFilter, '%')) = true)")
List<OAuth2ClientEntity> findEnabledByDomainNameAndPlatformType(@Param("domainName") String domainName,
@ -46,31 +46,33 @@ public interface OAuth2ClientRepository extends JpaRepository<OAuth2ClientEntity
@Query("SELECT c " +
"FROM OAuth2ClientEntity c " +
"LEFT JOIN MobileAppOauth2ClientEntity mc on c.id = mc.oauth2ClientId " +
"LEFT JOIN MobileAppEntity app on mc.mobileAppId = app.id " +
"WHERE app.pkgName = :pkgName AND app.oauth2Enabled = true " +
"LEFT JOIN MobileAppBundleOauth2ClientEntity ac ON c.id = ac.oauth2ClientId " +
"LEFT JOIN MobileAppBundleEntity b ON ac.mobileAppBundleId = b.id " +
"LEFT JOIN MobileAppEntity andApp ON b.androidAppId = andApp.id LEFT JOIN MobileAppEntity iosApp ON b.iosAppID = iosApp.id " +
"WHERE andApp.pkgName = :pkgName OR iosApp.pkgName = :pkgName AND b.oauth2Enabled = true " +
"AND (:platformFilter IS NULL OR c.platforms IS NULL OR c.platforms = '' OR ilike(c.platforms, CONCAT('%', :platformFilter, '%')) = true)")
List<OAuth2ClientEntity> findEnabledByPkgNameAndPlatformType(@Param("pkgName") String pkgName,
@Param("platformFilter") String platformFilter);
@Query("SELECT c " +
"FROM OAuth2ClientEntity c " +
"LEFT JOIN DomainOauth2ClientEntity dc on dc.oauth2ClientId = c.id " +
"LEFT JOIN DomainOauth2ClientEntity dc ON dc.oauth2ClientId = c.id " +
"WHERE dc.domainId = :domainId ")
List<OAuth2ClientEntity> findByDomainId(@Param("domainId") UUID domainId);
@Query("SELECT c " +
"FROM OAuth2ClientEntity c " +
"LEFT JOIN MobileAppOauth2ClientEntity mc on mc.oauth2ClientId = c.id " +
"WHERE mc.mobileAppId = :mobileAppId ")
List<OAuth2ClientEntity> findByMobileAppId(@Param("mobileAppId") UUID mobileAppId);
"LEFT JOIN MobileAppBundleOauth2ClientEntity bc ON bc.oauth2ClientId = c.id " +
"WHERE bc.mobileAppBundleId = :mobileAppBundleId ")
List<OAuth2ClientEntity> findByMobileAppBundleId(@Param("mobileAppBundleId") UUID mobileAppBundleId);
@Query("SELECT m.appSecret " +
"FROM MobileAppEntity m " +
"LEFT JOIN MobileAppOauth2ClientEntity mp on m.id = mp.mobileAppId " +
"LEFT JOIN OAuth2ClientEntity p on mp.oauth2ClientId = p.id " +
"WHERE p.id = :clientId " +
"AND m.pkgName = :pkgName")
@Query("SELECT a.appSecret " +
"FROM MobileAppEntity a " +
"LEFT JOIN MobileAppBundleEntity b ON (b.androidAppId = a.id OR b.iosAppID = a.id) " +
"LEFT JOIN MobileAppBundleOauth2ClientEntity bc ON bc.mobileAppBundleId = b.id " +
"LEFT JOIN OAuth2ClientEntity c ON bc.oauth2ClientId = c.id " +
"WHERE c.id = :clientId " +
"AND a.pkgName = :pkgName")
String findAppSecret(@Param("clientId") UUID id,
@Param("pkgName") String pkgName);

8
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java

@ -38,7 +38,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.mobile.MobileAppSettingsService;
import org.thingsboard.server.dao.mobile.QrCodeSettingService;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
@ -79,7 +79,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
@Autowired
private NotificationSettingsService notificationSettingsService;
@Autowired
private MobileAppSettingsService mobileAppSettingsService;
private QrCodeSettingService qrCodeSettingService;
@Autowired
private TenantDataValidator tenantValidator;
@Autowired
@ -164,7 +164,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
userService.deleteAllByTenantId(tenantId);
adminSettingsService.deleteAdminSettingsByTenantId(tenantId);
mobileAppSettingsService.deleteByTenantId(tenantId);
qrCodeSettingService.deleteByTenantId(tenantId);
notificationSettingsService.deleteNotificationSettings(tenantId);
tenantDao.removeById(tenantId, tenantId.getId());
@ -178,7 +178,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
EntityType.TB_RESOURCE, EntityType.OTA_PACKAGE, EntityType.RPC, EntityType.QUEUE,
EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE, EntityType.NOTIFICATION_TEMPLATE,
EntityType.NOTIFICATION_TARGET, EntityType.QUEUE_STATS, EntityType.CUSTOMER,
EntityType.DOMAIN, EntityType.MOBILE_APP, EntityType.OAUTH2_CLIENT
EntityType.DOMAIN, EntityType.MOBILE_APP_BUNDLE, EntityType.MOBILE_APP, EntityType.OAUTH2_CLIENT
);
}

14
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -43,7 +43,7 @@ import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.id.UserCredentialsId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.mobile.MobileSessionInfo;
import org.thingsboard.server.common.data.mobile.UserMobileInfo;
import org.thingsboard.server.common.data.mobile.UserMobileSessionInfo;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
@ -448,8 +448,8 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
public void saveMobileSession(TenantId tenantId, UserId userId, String mobileToken, MobileSessionInfo sessionInfo) {
removeMobileSession(tenantId, mobileToken); // unassigning fcm token from other users, in case we didn't clean up it on log out or mobile app uninstall
UserMobileInfo mobileInfo = findMobileInfo(tenantId, userId).orElseGet(() -> {
UserMobileInfo newMobileInfo = new UserMobileInfo();
UserMobileSessionInfo mobileInfo = findMobileSessionInfo(tenantId, userId).orElseGet(() -> {
UserMobileSessionInfo newMobileInfo = new UserMobileSessionInfo();
newMobileInfo.setSessions(new HashMap<>());
return newMobileInfo;
});
@ -459,12 +459,12 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
@Override
public Map<String, MobileSessionInfo> findMobileSessions(TenantId tenantId, UserId userId) {
return findMobileInfo(tenantId, userId).map(UserMobileInfo::getSessions).orElse(Collections.emptyMap());
return findMobileSessionInfo(tenantId, userId).map(UserMobileSessionInfo::getSessions).orElse(Collections.emptyMap());
}
@Override
public MobileSessionInfo findMobileSession(TenantId tenantId, UserId userId, String mobileToken) {
return findMobileInfo(tenantId, userId).map(mobileInfo -> mobileInfo.getSessions().get(mobileToken)).orElse(null);
return findMobileSessionInfo(tenantId, userId).map(mobileInfo -> mobileInfo.getSessions().get(mobileToken)).orElse(null);
}
@Override
@ -475,9 +475,9 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
}
}
private Optional<UserMobileInfo> findMobileInfo(TenantId tenantId, UserId userId) {
private Optional<UserMobileSessionInfo> findMobileSessionInfo(TenantId tenantId, UserId userId) {
return Optional.ofNullable(userSettingsService.findUserSettings(tenantId, userId, UserSettingsType.MOBILE))
.map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileInfo.class));
.map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileSessionInfo.class));
}
@Override

2
dao/src/main/resources/sql/schema-entities-idx.sql

@ -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_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);

37
dao/src/main/resources/sql/schema-entities.sql

@ -622,9 +622,27 @@ CREATE TABLE IF NOT EXISTS mobile_app (
id uuid NOT NULL CONSTRAINT mobile_app_pkey PRIMARY KEY,
created_time bigint NOT NULL,
tenant_id uuid,
pkg_name varchar(255) UNIQUE,
pkg_name varchar(255),
app_secret varchar(2048),
oauth2_enabled boolean
platform_type varchar(32),
status varchar(32),
version_info varchar(100000),
store_info varchar(16384),
CONSTRAINT mobile_app_pkg_name_platform_unq_key UNIQUE (pkg_name, platform_type)
);
CREATE TABLE IF NOT EXISTS mobile_app_bundle (
id uuid NOT NULL CONSTRAINT mobile_app_bundle_pkey PRIMARY KEY,
created_time bigint NOT NULL,
tenant_id uuid,
title varchar(255),
description varchar(1024),
android_app_id uuid UNIQUE,
ios_app_id uuid UNIQUE,
layout_config varchar(16384),
oauth2_enabled boolean,
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)
);
CREATE TABLE IF NOT EXISTS domain_oauth2_client (
@ -634,10 +652,10 @@ CREATE TABLE IF NOT EXISTS domain_oauth2_client (
CONSTRAINT fk_oauth2_client FOREIGN KEY (oauth2_client_id) REFERENCES oauth2_client(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS mobile_app_oauth2_client (
mobile_app_id uuid NOT NULL,
CREATE TABLE IF NOT EXISTS mobile_app_bundle_oauth2_client (
mobile_app_bundle_id uuid NOT NULL,
oauth2_client_id uuid NOT NULL,
CONSTRAINT fk_domain FOREIGN KEY (mobile_app_id) REFERENCES mobile_app(id) ON DELETE CASCADE,
CONSTRAINT fk_domain FOREIGN KEY (mobile_app_bundle_id) REFERENCES mobile_app_bundle(id) ON DELETE CASCADE,
CONSTRAINT fk_oauth2_client FOREIGN KEY (oauth2_client_id) REFERENCES oauth2_client(id) ON DELETE CASCADE
);
@ -876,13 +894,14 @@ CREATE TABLE IF NOT EXISTS queue_stats (
CONSTRAINT queue_stats_name_unq_key UNIQUE (tenant_id, queue_name, service_id)
);
CREATE TABLE IF NOT EXISTS mobile_app_settings (
CREATE TABLE IF NOT EXISTS qr_code_settings (
id uuid NOT NULL CONSTRAINT mobile_app_settings_pkey PRIMARY KEY,
created_time bigint NOT NULL,
tenant_id uuid NOT NULL,
use_default_app boolean,
android_config VARCHAR(1000),
ios_config VARCHAR(1000),
android_enabled boolean,
ios_enabled boolean,
mobile_app_bundle_id uuid,
qr_code_config VARCHAR(100000),
CONSTRAINT mobile_app_settings_tenant_id_unq_key UNIQUE (tenant_id)
CONSTRAINT qr_code_settings_tenant_id_unq_key UNIQUE (tenant_id)
);

48
dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java

@ -20,22 +20,18 @@ import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileApp;
import org.thingsboard.server.common.data.mobile.MobileAppInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.mobile.app.MobileAppStatus;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.oauth2.OAuth2ClientService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.thingsboard.server.dao.oauth2.OAuth2Utils.OAUTH2_AUTHORIZATION_PATH_TEMPLATE;
@DaoSqlTest
public class MobileAppServiceTest extends AbstractServiceTest {
@ -54,7 +50,7 @@ public class MobileAppServiceTest extends AbstractServiceTest {
@Test
public void testSaveMobileApp() {
MobileApp MobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "mobileApp.ce", true);
MobileApp MobileApp = validMobileApp(SYSTEM_TENANT_ID, "mobileApp.ce", PlatformType.IOS);
MobileApp savedMobileApp = mobileAppService.saveMobileApp(SYSTEM_TENANT_ID, MobileApp);
MobileApp retrievedMobileApp = mobileAppService.findMobileAppById(savedMobileApp.getTenantId(), savedMobileApp.getId());
@ -74,43 +70,23 @@ public class MobileAppServiceTest extends AbstractServiceTest {
@Test
public void testGetTenantMobileApps() {
List<MobileApp> MobileApps = new ArrayList<>();
List<MobileApp> mobileApps = new ArrayList<>();
for (int i = 0; i < 5; i++) {
MobileApp oAuth2Client = validMobileApp(TenantId.SYS_TENANT_ID, StringUtils.randomAlphabetic(5), true);
MobileApp oAuth2Client = validMobileApp(SYSTEM_TENANT_ID, StringUtils.randomAlphabetic(5), PlatformType.ANDROID);
MobileApp savedOauth2Client = mobileAppService.saveMobileApp(SYSTEM_TENANT_ID, oAuth2Client);
MobileApps.add(savedOauth2Client);
mobileApps.add(savedOauth2Client);
}
PageData<MobileAppInfo> retrieved = mobileAppService.findMobileAppInfosByTenantId(TenantId.SYS_TENANT_ID, new PageLink(10, 0));
List<MobileAppInfo> MobileAppInfos = MobileApps.stream().map(MobileApp -> new MobileAppInfo(MobileApp, Collections.emptyList())).toList();
assertThat(retrieved.getData()).containsOnlyOnceElementsOf(MobileAppInfos);
PageData<MobileApp> retrieved = mobileAppService.findMobileAppsByTenantId(TenantId.SYS_TENANT_ID, null, new PageLink(10, 0));
assertThat(retrieved.getData()).containsOnlyOnceElementsOf(mobileApps);
}
@Test
public void tesGetMobileAppInfo() {
OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "Test google client");
OAuth2Client savedOauth2Client = oAuth2ClientService.saveOAuth2Client(SYSTEM_TENANT_ID, oAuth2Client);
List<OAuth2ClientInfo> oAuth2ClientInfosByIds = oAuth2ClientService.findOAuth2ClientInfosByIds(TenantId.SYS_TENANT_ID, List.of(savedOauth2Client.getId()));
MobileApp MobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.app", true);
MobileApp savedMobileApp = mobileAppService.saveMobileApp(SYSTEM_TENANT_ID, MobileApp);
mobileAppService.updateOauth2Clients(TenantId.SYS_TENANT_ID, savedMobileApp.getId(), List.of(savedOauth2Client.getId()));
// check MobileApp info
MobileAppInfo retrievedInfo = mobileAppService.findMobileAppInfoById(SYSTEM_TENANT_ID, savedMobileApp.getId());
assertThat(retrievedInfo).isEqualTo(new MobileAppInfo(savedMobileApp, oAuth2ClientInfosByIds));
//find clients by MobileApp name
List<OAuth2ClientLoginInfo> oauth2LoginInfo = oAuth2ClientService.findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(savedMobileApp.getName(), null);
assertThat(oauth2LoginInfo).containsOnly(new OAuth2ClientLoginInfo(savedOauth2Client.getLoginButtonLabel(), savedOauth2Client.getLoginButtonIcon(), String.format(OAUTH2_AUTHORIZATION_PATH_TEMPLATE, savedOauth2Client.getUuidId().toString())));
}
private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, boolean oauth2Enabled) {
private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, PlatformType platformType) {
MobileApp MobileApp = new MobileApp();
MobileApp.setTenantId(tenantId);
MobileApp.setPkgName(mobileAppName);
MobileApp.setStatus(MobileAppStatus.DRAFT);
MobileApp.setAppSecret(StringUtils.randomAlphanumeric(24));
MobileApp.setOauth2Enabled(oauth2Enabled);
MobileApp.setPlatformType(platformType);
return MobileApp;
}
}

4
dao/src/test/resources/application-test.properties

@ -102,8 +102,8 @@ cache.specs.alarmTypes.maxSize=10000
cache.specs.userSettings.timeToLiveInMinutes=1440
cache.specs.userSettings.maxSize=10000
cache.specs.mobileAppSettings.timeToLiveInMinutes=1440
cache.specs.mobileAppSettings.maxSize=10000
cache.specs.qrCodeSettings.timeToLiveInMinutes=1440
cache.specs.qrCodeSettings.maxSize=10000
cache.specs.mobileSecretKey.timeToLiveInMinutes=1440
cache.specs.mobileSecretKey.maxSize=10000

64
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -107,6 +107,7 @@ import org.thingsboard.server.common.data.id.DomainId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
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.OAuth2ClientRegistrationTemplateId;
@ -123,8 +124,9 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.mobile.MobileApp;
import org.thingsboard.server.common.data.mobile.MobileAppInfo;
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.MobileAppBundleInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo;
@ -2107,12 +2109,12 @@ public class RestClient implements Closeable {
}, params).getBody();
}
public List<OAuth2ClientInfo> getTenantOAuth2Clients() {
public PageData<OAuth2ClientInfo> getTenantOAuth2Clients() {
return restTemplate.exchange(
baseURL + "/api/oauth2/client/infos",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<OAuth2ClientInfo>>() {
new ParameterizedTypeReference<PageData<OAuth2ClientInfo>>() {
}).getBody();
}
@ -2137,12 +2139,12 @@ public class RestClient implements Closeable {
restTemplate.delete(baseURL + "/api/oauth2/client/{id}", oAuth2ClientId.getId());
}
public List<DomainInfo> getTenantDomainInfos() {
public PageData<DomainInfo> getTenantDomainInfos() {
return restTemplate.exchange(
baseURL + "/api/domain/infos",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<DomainInfo>>() {
new ParameterizedTypeReference<PageData<DomainInfo>>() {
}).getBody();
}
@ -2171,19 +2173,19 @@ public class RestClient implements Closeable {
restTemplate.postForLocation(baseURL + "/api/domain/{id}/oauth2Clients", oauth2ClientIds, domainId.getId());
}
public List<DomainInfo> getTenantMobileAppInfos() {
public PageData<MobileApp> getTenantMobileApps() {
return restTemplate.exchange(
baseURL + "/api/mobileApp/infos",
baseURL + "/api/mobile/app",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<DomainInfo>>() {
new ParameterizedTypeReference<PageData<MobileApp>>() {
}).getBody();
}
public Optional<MobileAppInfo> getMobileAppInfoById(MobileAppId mobileAppId) {
public Optional<MobileApp> getMobileAppById(MobileAppId mobileAppId) {
try {
ResponseEntity<MobileAppInfo> mobileAppInfo = restTemplate.getForEntity(baseURL + "/api/mobileApp/info/{id}", MobileAppInfo.class, mobileAppId.getId());
return Optional.ofNullable(mobileAppInfo.getBody());
ResponseEntity<MobileApp> mobileApp = restTemplate.getForEntity(baseURL + "/api/mobile/app/{id}", MobileApp.class, mobileAppId.getId());
return Optional.ofNullable(mobileApp.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
@ -2194,15 +2196,45 @@ public class RestClient implements Closeable {
}
public MobileApp saveMobileApp(MobileApp mobileApp) {
return restTemplate.postForEntity(baseURL + "/api/mobileApp", mobileApp, MobileApp.class).getBody();
return restTemplate.postForEntity(baseURL + "/api/mobile/app", mobileApp, MobileApp.class).getBody();
}
public void deleteMobileApp(MobileAppId mobileAppId) {
restTemplate.delete(baseURL + "/api/mobileApp/{id}", mobileAppId.getId());
restTemplate.delete(baseURL + "/api/mobile/app/{id}", mobileAppId.getId());
}
public void updateMobileAppOauth2Clients(MobileAppId mobileAppId, UUID[] oauth2ClientIds) {
restTemplate.postForLocation(baseURL + "/api/mobileApp/{id}/oauth2Clients", oauth2ClientIds, mobileAppId.getId());
public PageData<MobileAppBundleInfo> getTenantMobileBundleInfos() {
return restTemplate.exchange(
baseURL + "/api/mobile/bundle/infos",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<MobileAppBundleInfo>>() {
}).getBody();
}
public Optional<MobileAppBundle> getMobileBundleById(MobileAppBundleId mobileAppBundleId) {
try {
ResponseEntity<MobileAppBundle> mobileApp = restTemplate.getForEntity(baseURL + "/api/mobile/bundle/{id}", MobileAppBundle.class, mobileAppBundleId.getId());
return Optional.ofNullable(mobileApp.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public MobileAppBundle saveMobileBundle(MobileAppBundle mobileAppBundle) {
return restTemplate.postForEntity(baseURL + "/api/mobile/bundle", mobileAppBundle, MobileAppBundle.class).getBody();
}
public void deleteMobileBundle(MobileAppBundleId mobileAppBundleId) {
restTemplate.delete(baseURL + "/api/mobile/bundle/{id}", mobileAppBundleId.getId());
}
public void updateMobileAppBundleOauth2Clients(MobileAppBundleId mobileAppBundleId, UUID[] oauth2ClientIds) {
restTemplate.postForLocation(baseURL + "/api/mobile/bundle/{id}/oauth2Clients", oauth2ClientIds, mobileAppBundleId.getId());
}
public String getLoginProcessingUrl() {

3
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -61,6 +61,7 @@ import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.mobile.MobileAppBundleService;
import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.nosql.CassandraStatementTask;
import org.thingsboard.server.dao.nosql.TbResultSetFuture;
@ -355,6 +356,8 @@ public interface TbContext {
MobileAppService getMobileAppService();
MobileAppBundleService getMobileAppBundleService();
SlackService getSlackService();
boolean isExternalNodeForceAck();

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.DomainId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationRuleId;
@ -157,6 +158,9 @@ public class TenantIdLoader {
case MOBILE_APP:
tenantEntity = ctx.getMobileAppService().findMobileAppById(ctxTenantId, new MobileAppId(id));
break;
case MOBILE_APP_BUNDLE:
tenantEntity = ctx.getMobileAppBundleService().findMobileAppBundleById(ctxTenantId, new MobileAppBundleId(id));
break;
default:
throw new RuntimeException("Unexpected entity type: " + entityId.getEntityType());
}

12
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java

@ -52,7 +52,8 @@ import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.mobile.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.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
@ -72,6 +73,7 @@ import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.domain.DomainService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.mobile.MobileAppBundleService;
import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
@ -151,6 +153,8 @@ public class TenantIdLoaderTest {
private DomainService domainService;
@Mock
private MobileAppService mobileAppService;
@Mock
private MobileAppBundleService mobileAppBundleService;
private TenantId tenantId;
private TenantProfileId tenantProfileId;
@ -392,6 +396,12 @@ public class TenantIdLoaderTest {
when(ctx.getMobileAppService()).thenReturn(mobileAppService);
doReturn(mobileApp).when(mobileAppService).findMobileAppById(eq(tenantId), any());
break;
case MOBILE_APP_BUNDLE:
MobileAppBundle mobileAppBundle = new MobileAppBundle();
mobileAppBundle.setTenantId(tenantId);
when(ctx.getMobileAppBundleService()).thenReturn(mobileAppBundleService);
doReturn(mobileAppBundle).when(mobileAppBundleService).findMobileAppBundleById(eq(tenantId), any());
break;
default:
throw new RuntimeException("Unexpected originator EntityType " + entityType);
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save