Browse Source

mobile app bundles: initial implementation

pull/11817/head
dashevchenko 2 years ago
parent
commit
04bba3ff5a
  1. 50
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  2. 36
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  3. 132
      application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java
  4. 43
      application/src/main/java/org/thingsboard/server/controller/MobileAppController.java
  5. 65
      application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java
  6. 60
      application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java
  7. 10
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  8. 83
      application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppBundleService.java
  9. 18
      application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java
  10. 32
      application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppBundleService.java
  11. 7
      application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java
  12. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  13. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
  14. 2
      application/src/main/resources/thingsboard.yml
  15. 149
      application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java
  16. 107
      application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java
  17. 253
      application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java
  18. 258
      application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java
  19. 47
      common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java
  20. 14
      common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java
  21. 2
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  22. 3
      common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
  23. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  24. 39
      common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java
  25. 8
      common/data/src/main/java/org/thingsboard/server/common/data/id/QrCodeSettingsId.java
  26. 11
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidQrCodeConfig.java
  27. 10
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosQrCodeConfig.java
  28. 22
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java
  29. 84
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundle.java
  30. 18
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleInfo.java
  31. 6
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleOauth2Client.java
  32. 24
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppStatus.java
  33. 40
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLayoutConfig.java
  34. 28
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLoginInfo.java
  35. 41
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuItem.java
  36. 32
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuPath.java
  37. 29
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileUserInfo.java
  38. 37
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeConfig.java
  39. 18
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeSettings.java
  40. 120
      dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java
  41. 43
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.java
  42. 170
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java
  43. 15
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java
  44. 76
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java
  45. 14
      dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java
  46. 128
      dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java
  47. 10
      dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsCaffeineCache.java
  48. 6
      dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsDao.java
  49. 2
      dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsEvictEvent.java
  50. 10
      dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsRedisCache.java
  51. 31
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  52. 107
      dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java
  53. 33
      dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleOauth2ClientEntity.java
  54. 35
      dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java
  55. 2
      dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientCompositeKey.java
  56. 85
      dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java
  57. 81
      dao/src/main/java/org/thingsboard/server/dao/model/sql/QrCodeSettingsEntity.java
  58. 2
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java
  59. 51
      dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java
  60. 42
      dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java
  61. 95
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java
  62. 34
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java
  63. 24
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaQrCodeSettingsDao.java
  64. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleOauth2ClientRepository.java
  65. 51
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java
  66. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppRepository.java
  67. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/QrCodeSettingsRepository.java
  68. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java
  69. 26
      dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java
  70. 8
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  71. 28
      dao/src/main/resources/sql/schema-entities.sql
  72. 35
      dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java
  73. 4
      dao/src/test/resources/application-test.properties
  74. 51
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

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

@ -50,6 +50,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;
@ -86,6 +87,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,6 +103,7 @@ 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.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;
@ -134,6 +137,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;
@ -197,6 +201,11 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@TbCoreComponent
public abstract class BaseController {
public static final String DASHBOARD_ID = "dashboardId";
public static final String HOME_DASHBOARD_ID = "homeDashboardId";
public static final String HOME_DASHBOARD_HIDE_TOOLBAR = "homeDashboardHideToolbar";
protected final Logger log = org.slf4j.LoggerFactory.getLogger(getClass());
/*Swagger UI description*/
@ -261,6 +270,9 @@ public abstract class BaseController {
@Autowired
protected MobileAppService mobileAppService;
@Autowired
protected MobileAppBundleService mobileAppBundleService;
@Autowired
protected OAuth2ConfigTemplateService oAuth2ConfigTemplateService;
@ -644,6 +656,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);
}
@ -832,6 +847,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);
}
@ -910,6 +929,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()) {

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

@ -0,0 +1,132 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import 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.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.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_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 apps for ANDROID and IOS platforms." +
"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. " +
"Specify existing Mobile App Bundle Id to configure application settings like oauth2 settings, self-registration or layout settings. " +
"Referencing non-existing Mobile App Bundle Id will cause 'Not Found' error." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_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. ")
@PreAuthorize("hasAnyAuthority('SYS_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_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_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 {
accessControlService.checkPermission(getCurrentUser(), Resource.MOBILE_APP, Operation.READ);
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_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_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_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_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());
}
}

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

@ -16,8 +16,6 @@
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;
@ -26,17 +24,13 @@ 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.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.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.config.annotations.ApiOperation;
@ -45,7 +39,6 @@ import org.thingsboard.server.service.entitiy.mobile.TbMobileAppService;
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;
@ -69,35 +62,21 @@ public class MobileAppController extends BaseController {
"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)
"\n\nThe pair of mobile app package name and platform type is unique for entire platform setup.\n\n" + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@PostMapping(value = "/mobileApp")
@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());
}
@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());
return tbMobileAppService.save(mobileApp, 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)
@GetMapping(value = "/mobile/app")
public PageData<MobileApp> getTenantMobileAppInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ -109,21 +88,21 @@ public class MobileAppController extends BaseController {
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.MOBILE_APP, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return mobileAppService.findMobileAppInfosByTenantId(getTenantId(), pageLink);
return mobileAppService.findMobileAppsByTenantId(getTenantId(), 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 {
@GetMapping(value = "/mobile/app/{id}")
public MobileApp getMobileAppInfoById(@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}")
@DeleteMapping(value = "/mobile/app/{id}")
public void deleteMobileApp(@PathVariable UUID id) throws Exception {
MobileAppId mobileAppId = new MobileAppId(id);
MobileApp mobileApp = checkMobileAppId(mobileAppId, Operation.DELETE);

65
application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java

@ -0,0 +1,65 @@
/**
* 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.Schema;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.HomeDashboardInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.mobile.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.MobileLoginInfo;
import org.thingsboard.server.common.data.mobile.MobileUserInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.List;
@RequiredArgsConstructor
@RestController
@TbCoreComponent
public class MobileV2Controller extends BaseController {
@GetMapping(value = "/api/noauth/mobile")
public MobileLoginInfo getMobileUserLoginSettings(@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);
return new MobileLoginInfo(oauth2Clients);
}
@GetMapping(value = "/api/auth/mobile")
public MobileUserInfo getMobileUserSettings(@Parameter(description = "Mobile application package name")
@RequestParam String pkgName,
@Parameter(description = "Platform type",
schema = @Schema(allowableValues = {"ANDROID", "IOS"}))
@RequestParam PlatformType platform) throws ThingsboardException {
SecurityUser securityUser = getCurrentUser();
User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId());
HomeDashboardInfo homeDashboardInfo = getHomeDashboardInfo(securityUser, user.getAdditionalInfo());
MobileAppBundle mobileAppBundle = mobileAppBundleService.findMobileAppBundleByPkgNameAndPlatform(securityUser.getTenantId(), pkgName, platform);
return new MobileUserInfo(user, homeDashboardInfo, mobileAppBundle != null ? mobileAppBundle.getLayoutConfig() : null);
}
}

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

@ -32,12 +32,13 @@ 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.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.AndroidQrCodeConfig;
import org.thingsboard.server.common.data.mobile.IosQrCodeConfig;
import org.thingsboard.server.common.data.mobile.QrCodeSettings;
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;
@ -54,7 +55,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHO
@RequiredArgsConstructor
@RestController
@TbCoreComponent
public class MobileApplicationController extends BaseController {
public class QrCodeSettingsController extends BaseController {
@Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}")
private int mobileSecretKeyTtl;
@ -89,15 +90,15 @@ public class MobileApplicationController extends BaseController {
private final SystemSecurityService systemSecurityService;
private final MobileAppSecretService mobileAppSecretService;
private final MobileAppSettingsService mobileAppSettingsService;
private final QrCodeSettingService qrCodeSettingService;
private final MobileAppService mobileAppService;
@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())));
AndroidQrCodeConfig androidQrConfig = qrCodeSettingService.getAndroidQrCodeConfig(TenantId.SYS_TENANT_ID);
if (androidQrConfig != null && androidQrConfig.isEnabled()) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, androidQrConfig.getAppPackage(), androidQrConfig.getSha256CertFingerprints())));
} else {
return ResponseEntity.notFound().build();
}
@ -106,10 +107,9 @@ 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())));
IosQrCodeConfig iosQrCodeConfig = qrCodeSettingService.getIosQrCodeConfig(TenantId.SYS_TENANT_ID);
if (iosQrCodeConfig != null && iosQrCodeConfig.isEnabled() && iosQrCodeConfig.getAppId() != null) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, iosQrCodeConfig.getAppId())));
} else {
return ResponseEntity.notFound().build();
}
@ -118,36 +118,36 @@ 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/qr/settings")
public QrCodeSettings saveMobileAppSettings(@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/qr/settings")
public QrCodeSettings getMobileAppSettings() throws ThingsboardException {
SecurityUser currentUser = getCurrentUser();
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ);
return mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
return qrCodeSettingService.getQrCodeSettings(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/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);
QrCodeSettings qrCodeSettings = qrCodeSettingService.getQrCodeSettings(TenantId.SYS_TENANT_ID);
String appDomain;
if (!mobileAppSettings.isUseDefaultApp()) {
if (!qrCodeSettings.isUseDefaultApp()) {
appDomain = platformDomain;
} else {
appDomain = defaultAppDomain;
@ -170,15 +170,17 @@ 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();
QrCodeSettings qrCodeSettings = qrCodeSettingService.getQrCodeSettings(TenantId.SYS_TENANT_ID);
boolean useDefaultApp = qrCodeSettings.isUseDefaultApp();
if (userAgent.contains("Android")) {
String googlePlayLink = useDefaultApp ? qrCodeSettings.getDefaultGooglePlayLink() :
mobileAppService.findAndroidQrCodeConfig(TenantId.SYS_TENANT_ID, qrCodeSettings.getMobileAppBundleId()).getStoreLink();
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", googlePlayLink)
.build();
} else if (userAgent.contains("iPhone") || userAgent.contains("iPad")) {
String appStoreLink = useDefaultApp ? qrCodeSettings.getDefaultAppStoreLink() :
mobileAppService.findIosQrCodeConfig(TenantId.SYS_TENANT_ID, qrCodeSettings.getMobileAppBundleId()).getStoreLink();
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", appStoreLink)
.build();

10
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.QrCodeSettings;
import org.thingsboard.server.common.data.mobile.QRCodeConfig;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.mobile.MobileAppSettingsService;
import org.thingsboard.server.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.getQrCodeSettings(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.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;
}
}
}

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

@ -37,14 +37,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 +50,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.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);
}

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

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

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

@ -37,6 +37,7 @@ public class SysAdminPermissions extends AbstractPermissions {
put(Resource.WIDGET_TYPE, systemEntityPermissionChecker);
put(Resource.OAUTH2_CLIENT, PermissionChecker.allowAllPermissionChecker);
put(Resource.MOBILE_APP, PermissionChecker.allowAllPermissionChecker);
put(Resource.MOBILE_APP_BUNDLE, PermissionChecker.allowAllPermissionChecker);
put(Resource.DOMAIN, PermissionChecker.allowAllPermissionChecker);
put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, PermissionChecker.allowAllPermissionChecker);
put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker);

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

@ -609,7 +609,7 @@ 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:
qrCodeSettings:
timeToLiveInMinutes: "${CACHE_SPECS_MOBILE_APP_SETTINGS_TTL:1440}" # Mobile application cache TTL
maxSize: "${CACHE_SPECS_MOBILE_APP_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled
mobileSecretKey:

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

@ -0,0 +1,149 @@
/**
* 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.MobileApp;
import org.thingsboard.server.common.data.mobile.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.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.List;
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, true);
androidApp = doPost("/api/mobile/app", androidApp, MobileApp.class);
iosApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.ios.package", PlatformType.IOS, true);
iosApp = doPost("/api/mobile/app", iosApp, MobileApp.class);
}
@After
public void tearDown() throws Exception {
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<MobileAppBundleInfo> pageData2 = doGetTypedWithPageLink("/api/mobile/bundle?", 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<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 testSaveMobileApp() throws Exception {
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 testUpdateMobileAppOauth2Clients() 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 retrievedMobileAppInfo = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedAppBundle.getId().getId());
assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppBundleInfo(savedAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), List.of(new OAuth2ClientInfo(oAuth2Client))));
doPut("/api/mobile/bundle/" + savedAppBundle.getId() + "/oauth2Clients", List.of(savedOAuth2Client2.getId().getId()));
MobileAppBundleInfo retrievedMobileAppInfo2 = doGet("/api/mobileApp/info/{id}", MobileAppBundleInfo.class, savedOAuth2Client.getId().getId());
assertThat(retrievedMobileAppInfo2).isEqualTo(new MobileAppBundleInfo(savedAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), 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/mobileApp/info/{id}", MobileAppBundleInfo.class, savedMobileAppBundle.getId().getId());
assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppBundleInfo(savedMobileAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), List.of(new OAuth2ClientInfo(savedOAuth2Client))));
}
private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, PlatformType platformType, boolean oauth2Enabled) {
MobileApp MobileApp = new MobileApp();
MobileApp.setTenantId(tenantId);
MobileApp.setPkgName(mobileAppName);
MobileApp.setPlatformType(platformType);
MobileApp.setAppSecret(StringUtils.randomAlphanumeric(24));
return MobileApp;
}
}

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

@ -22,17 +22,13 @@ 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.AndroidQrCodeConfig;
import org.thingsboard.server.common.data.mobile.IosQrCodeConfig;
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.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 +37,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,88 +47,91 @@ 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(TenantId.SYS_TENANT_ID, "my.test.package");
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(TenantId.SYS_TENANT_ID, "mobileApp.ce");
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()));
public void testShouldNotSaveMobileAppWithWrongQrCodeConf() throws Exception {
MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "mobileApp.ce");
AndroidQrCodeConfig androidQrCodeConfig = AndroidQrCodeConfig.builder()
.enabled(true)
.appPackage(null)
.sha256CertFingerprints(null)
.build();
mobileApp.setQrCodeConfig(androidQrCodeConfig);
doPost("/api/mobile/app", mobileApp)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Validation error: appPackage must not be blank, sha256CertFingerprints must not be blank, storeLink must not be blank")));
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))));
androidQrCodeConfig.setAppPackage("test_app_package");
doPost("/api/mobile/app", mobileApp)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Validation error: sha256CertFingerprints must not be blank, storeLink must not be blank")));
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))));
androidQrCodeConfig.setSha256CertFingerprints("test_sha_256");
androidQrCodeConfig.setStoreLink("https://store.com");
doPost("/api/mobile/app", mobileApp)
.andExpect(status().isOk());
}
@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);
public void testShouldNotSaveMobileAppWithWrongIosConf() throws Exception {
MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "mobileApp.ce");
IosQrCodeConfig iosQrCodeConfig = IosQrCodeConfig.builder()
.enabled(true)
.appId(null)
.build();
mobileApp.setQrCodeConfig(iosQrCodeConfig);
doPost("/api/mobile/app", mobileApp)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Validation error: appId must not be blank, storeLink must not be blank")));
MobileAppInfo retrievedMobileAppInfo = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId());
assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppInfo(savedMobileApp, List.of(new OAuth2ClientInfo(savedOAuth2Client))));
iosQrCodeConfig.setAppId("test_app_id");
iosQrCodeConfig.setStoreLink("https://store.com");
doPost("/api/mobile/app", mobileApp)
.andExpect(status().isOk());
}
private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, boolean oauth2Enabled) {
private MobileApp validMobileApp(TenantId tenantId, String mobileAppName) {
MobileApp MobileApp = new MobileApp();
MobileApp.setTenantId(tenantId);
MobileApp.setPkgName(mobileAppName);
MobileApp.setAppSecret(StringUtils.randomAlphanumeric(24));
MobileApp.setOauth2Enabled(oauth2Enabled);
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");
}
}

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

@ -0,0 +1,258 @@
/**
* 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.id.TenantId;
import org.thingsboard.server.common.data.mobile.AndroidQrCodeConfig;
import org.thingsboard.server.common.data.mobile.IosQrCodeConfig;
import org.thingsboard.server.common.data.mobile.MobileApp;
import org.thingsboard.server.common.data.mobile.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.MobileAppBundleInfo;
import org.thingsboard.server.common.data.mobile.QrCodeSettings;
import org.thingsboard.server.common.data.mobile.QRCodeConfig;
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(TenantId.SYS_TENANT_ID, "my.android.package", PlatformType.ANDROID, true);
AndroidQrCodeConfig androidQrCodeConfig = AndroidQrCodeConfig.builder()
.appPackage(ANDROID_PACKAGE_NAME)
.sha256CertFingerprints(ANDROID_APP_SHA256)
.storeLink(ANDROID_STORE_LINK)
.enabled(true)
.build();
androidApp.setQrCodeConfig(androidQrCodeConfig);
MobileApp savedAndroidApp = doPost("/api/mobile/app", androidApp, MobileApp.class);
MobileApp iosApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.ios.package", PlatformType.IOS, true);
IosQrCodeConfig iosQrCodeConfig = IosQrCodeConfig.builder()
.appId(APPLE_APP_ID)
.enabled(true)
.storeLink(IOS_STORE_LINK)
.build();
iosApp.setQrCodeConfig(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/qr/settings", QrCodeSettings.class);
QRCodeConfig qrCodeConfig = new QRCodeConfig();
qrCodeConfig.setQrCodeLabel(TEST_LABEL);
qrCodeSettings.setUseDefaultApp(true);
qrCodeSettings.setMobileAppBundleId(null);
qrCodeSettings.setQrCodeConfig(qrCodeConfig);
doPost("/api/qr/settings", qrCodeSettings)
.andExpect(status().isOk());
}
@After
public void tearDown() throws Exception {
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<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());
}
}
@Test
public void testSaveQrCodeSettings() throws Exception {
loginSysAdmin();
QrCodeSettings qrCodeSettings = doGet("/api/qr/settings", QrCodeSettings.class);
assertThat(qrCodeSettings.getQrCodeConfig().getQrCodeLabel()).isEqualTo(TEST_LABEL);
assertThat(qrCodeSettings.isUseDefaultApp()).isTrue();
qrCodeSettings.setUseDefaultApp(false);
qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId());
doPost("/api/qr/settings", qrCodeSettings)
.andExpect(status().isOk());
QrCodeSettings updatedQrCodeSettings = doGet("/api/qr/settings", QrCodeSettings.class);
assertThat(updatedQrCodeSettings.isUseDefaultApp()).isFalse();
}
@Test
public void testShouldNotSaveQrCodeSettingsWithoutRequiredConfig() throws Exception {
loginSysAdmin();
QrCodeSettings qrCodeSettings = doGet("/api/qr/settings", QrCodeSettings.class);
qrCodeSettings.setUseDefaultApp(false);
qrCodeSettings.setQrCodeConfig(null);
qrCodeSettings.setMobileAppBundleId(null);
doPost("/api/qr/settings", qrCodeSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Bundle required to use custom application!")));
qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId());
doPost("/api/qr/settings", qrCodeSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Qr code configuration is required!")));
qrCodeSettings.setQrCodeConfig(QRCodeConfig.builder().showOnHomePage(false).build());
doPost("/api/qr/settings", qrCodeSettings)
.andExpect(status().isOk());
}
@Test
public void testShouldSaveQrCodeSettingsForDefaultApp() throws Exception {
loginSysAdmin();
QrCodeSettings qrCodeSettings = doGet("/api/qr/settings", QrCodeSettings.class);
qrCodeSettings.setUseDefaultApp(true);
qrCodeSettings.setMobileAppBundleId(null);
doPost("/api/qr/settings", qrCodeSettings)
.andExpect(status().isOk());
}
@Test
public void testGetApplicationAssociations() throws Exception {
loginSysAdmin();
QrCodeSettings qrCodeSettings = doGet("/api/qr/settings", QrCodeSettings.class);
qrCodeSettings.setUseDefaultApp(false);
doPost("/api/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(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/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/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/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/qr/settings", QrCodeSettings.class);
qrCodeSettings.setUseDefaultApp(false);
doPost("/api/mobile/app/settings", qrCodeSettings);
String customAppDeepLink = doGet("/api/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/qr/deepLink", String.class);
Matcher tenantCustomAppParsedDeepLink = customAppExpectedPattern.matcher(tenantCustomAppDeepLink);
assertThat(tenantCustomAppParsedDeepLink.matches()).isTrue();
assertThat(tenantCustomAppParsedDeepLink.group(1)).isEqualTo("localhost");
loginCustomerUser();
String customerCustomAppDeepLink = doGet("/api/qr/deepLink", String.class);
Matcher customerCustomAppParsedDeepLink = customAppExpectedPattern.matcher(customerCustomAppDeepLink);
assertThat(customerCustomAppParsedDeepLink.matches()).isTrue();
assertThat(customerCustomAppParsedDeepLink.group(1)).isEqualTo("localhost");
}
private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, PlatformType platformType, boolean oauth2Enabled) {
MobileApp MobileApp = new MobileApp();
MobileApp.setTenantId(tenantId);
MobileApp.setPkgName(mobileAppName);
MobileApp.setPlatformType(platformType);
MobileApp.setAppSecret(StringUtils.randomAlphanumeric(24));
return MobileApp;
}
}

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

@ -0,0 +1,47 @@
/**
* 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.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.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 deleteMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId);
MobileAppBundle findMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId);
PageData<MobileAppBundleInfo> findMobileAppBundleInfosByTenantId(TenantId tenantId, PageLink pageLink);
MobileAppBundleInfo findMobileAppBundleInfoById(TenantId tenantId, MobileAppBundleId mobileAppBundleId);
void updateOauth2Clients(TenantId tenantId, MobileAppBundleId mobileAppBundleId, List<OAuth2ClientId> oAuth2ClientIds);
MobileAppBundle findMobileAppBundleByPkgNameAndPlatform(TenantId tenantId, String pkgName, PlatformType platform);
void deleteMobileAppsByTenantId(TenantId tenantId);
}

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

@ -15,17 +15,16 @@
*/
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.AndroidQrCodeConfig;
import org.thingsboard.server.common.data.mobile.IosQrCodeConfig;
import org.thingsboard.server.common.data.mobile.MobileApp;
import org.thingsboard.server.common.data.mobile.MobileAppInfo;
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);
@ -34,11 +33,12 @@ public interface MobileAppService extends EntityDaoService {
MobileApp findMobileAppById(TenantId tenantId, MobileAppId mobileAppId);
PageData<MobileAppInfo> findMobileAppInfosByTenantId(TenantId tenantId, PageLink pageLink);
PageData<MobileApp> findMobileAppsByTenantId(TenantId tenantId, PageLink pageLink);
MobileAppInfo findMobileAppInfoById(TenantId tenantId, MobileAppId mobileAppId);
AndroidQrCodeConfig findAndroidQrCodeConfig(TenantId tenantId, MobileAppBundleId mobileAppBundleId);
void updateOauth2Clients(TenantId tenantId, MobileAppId mobileAppId, List<OAuth2ClientId> oAuth2ClientIds);
IosQrCodeConfig findIosQrCodeConfig(TenantId tenantId, MobileAppBundleId mobileAppBundleId);
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(37);
@Getter
private final int protoNumber; // Corresponds to EntityTypeProto

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));
}
}

11
common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidQrCodeConfig.java

@ -15,11 +15,13 @@
*/
package org.thingsboard.server.common.data.mobile;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.validation.NoXss;
@Data
@ -27,14 +29,21 @@ import org.thingsboard.server.common.data.validation.NoXss;
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class AndroidConfig {
public class AndroidQrCodeConfig implements QrCodeConfig {
private boolean enabled;
@NoXss
@NotBlank
private String appPackage;
@NoXss
@NotBlank
private String sha256CertFingerprints;
@NoXss
@NotBlank
private String storeLink;
@Override
public PlatformType getType() {
return PlatformType.ANDROID;
}
}

10
common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosQrCodeConfig.java

@ -15,11 +15,13 @@
*/
package org.thingsboard.server.common.data.mobile;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.validation.NoXss;
@Data
@ -27,12 +29,18 @@ import org.thingsboard.server.common.data.validation.NoXss;
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class IosConfig {
public class IosQrCodeConfig implements QrCodeConfig {
private boolean enabled;
@NoXss
@NotBlank
private String appId;
@NoXss
@NotBlank
private String storeLink;
@Override
public PlatformType getType() {
return PlatformType.IOS;
}
}

22
common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java

@ -16,7 +16,9 @@
package org.thingsboard.server.common.data.mobile;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
@ -27,7 +29,9 @@ 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;
import org.thingsboard.server.common.data.validation.NoXss;
@EqualsAndHashCode(callSuper = true)
@Data
@ -44,8 +48,17 @@ 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)
private PlatformType platformType;
@Schema(description = "Application status: PUBLISHED, DEPRECATED, SUSPENDED", requiredMode = Schema.RequiredMode.REQUIRED)
private MobileAppStatus status;
@Schema(description = "Application version info")
@NoXss
@Length(fieldName = "versionInfo", max = 16384)
private JsonNode versionInfo;
@Schema(description = "Application qr code configuration")
@Valid
private QrCodeConfig qrCodeConfig;
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.qrCodeConfig = mobile.qrCodeConfig;
}
@Override

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

@ -0,0 +1,84 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.mobile;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
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.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
@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;
}
}

18
common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppInfo.java → common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleInfo.java

@ -18,7 +18,7 @@ package org.thingsboard.server.common.data.mobile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import java.util.List;
@ -26,22 +26,28 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
@Schema
public class MobileAppInfo extends MobileApp {
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;
public MobileAppInfo(MobileApp mobileApp, List<OAuth2ClientInfo> oauth2ClientInfos) {
public MobileAppBundleInfo(MobileAppBundle mobileApp, String androidPkgName, String iosPkgName, List<OAuth2ClientInfo> oauth2ClientInfos) {
super(mobileApp);
this.androidPkgName = androidPkgName;
this.iosPkgName = iosPkgName;
this.oauth2ClientInfos = oauth2ClientInfos;
}
public MobileAppInfo() {
public MobileAppBundleInfo() {
super();
}
public MobileAppInfo(MobileAppId mobileAppId) {
super(mobileAppId);
public MobileAppBundleInfo(MobileAppBundleId mobileAppBundleId) {
super(mobileAppBundleId);
}
}

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

@ -18,15 +18,15 @@ package org.thingsboard.server.common.data.mobile;
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;
}

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

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

40
common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLayoutConfig.java

@ -0,0 +1,40 @@
/**
* 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 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 java.util.ArrayList;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class MobileLayoutConfig {
@Schema(description = "List of custom menu items", requiredMode = Schema.RequiredMode.REQUIRED)
@Valid
private List<MobileMenuItem> items = new ArrayList<>();
}

28
common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLoginInfo.java

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

41
common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuItem.java

@ -0,0 +1,41 @@
/**
* 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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class MobileMenuItem {
@Schema(description = "Menu item label", example = "Ar quality", requiredMode = Schema.RequiredMode.REQUIRED)
private String label;
@Schema(description = "URL of the menu item icon", example = "home_icon")
private String icon;
@Schema(description = "Path to open, when user clicks the menu item", example = "/dashboard")
private MobileMenuPath path;
@Schema(description = "Id of the resource to open, when user clicks the menu item", example = "8a8d81b0-5975-11ef-83b1-d3209c242a36")
private String id;
}

32
common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuPath.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.common.data.mobile;
public enum MobileMenuPath {
HOME,
ASSETS,
DEVICES,
DEVICE_LIST,
ALARMS,
DASHBOARDS,
DASHBOARD,
AUDIT_LOGS,
CUSTOMERS,
CUSTOMER,
NOTIFICATION,
CUSTOM
}

29
common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileUserInfo.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;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.thingsboard.server.common.data.HomeDashboardInfo;
import org.thingsboard.server.common.data.User;
@Data
@AllArgsConstructor
public class MobileUserInfo {
User user;
HomeDashboardInfo homeDashboardInfo;
MobileLayoutConfig layoutConfig;
}

37
common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeConfig.java

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

18
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.java

@ -22,13 +22,15 @@ 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.MobileAppId;
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;
@ -36,12 +38,8 @@ public class MobileAppSettings extends BaseData<MobileAppSettingsId> implements
private TenantId tenantId;
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, 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;
@Schema(description = "Mobile app bundle.")
private MobileAppBundleId mobileAppBundleId;
@Valid
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "QR code config configuration.")
private QRCodeConfig qrCodeConfig;
@ -52,10 +50,10 @@ public class MobileAppSettings extends BaseData<MobileAppSettingsId> implements
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String defaultAppStoreLink;
public MobileAppSettings() {
public QrCodeSettings() {
}
public MobileAppSettings(MobileAppSettingsId id) {
public QrCodeSettings(QrCodeSettingsId id) {
super(id);
}

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;
}
}

43
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.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.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.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.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<MobileAppBundle> findByTenantId(TenantId tenantId, PageLink pageLink);
List<MobileAppBundleOauth2Client> findOauth2ClientsByMobileAppBundleId(TenantId tenantId, MobileAppBundleId mobileAppBundleId);
void addOauth2Client(MobileAppBundleOauth2Client mobileAppBundleOauth2Client);
void removeOauth2Client(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.MobileApp;
import org.thingsboard.server.common.data.mobile.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.MobileAppBundleInfo;
import org.thingsboard.server.common.data.mobile.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 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 MobileAppBundleServiceImpl extends AbstractEntityService implements MobileAppBundleService {
@Autowired
private OAuth2ClientDao oauth2ClientDao;
@Autowired
private MobileAppBundleDao mobileAppBundleDao;
@Autowired
private MobileAppService mobileAppService;
@Override
public MobileAppBundle saveMobileAppBundle(TenantId tenantId, MobileAppBundle mobileApp) {
log.trace("Executing saveMobileApp [{}]", mobileApp);
try {
MobileAppBundle savedMobileApp = mobileAppBundleDao.save(tenantId, mobileApp);
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 deleteMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId) {
log.trace("Executing deleteMobileAppById [{}]", mobileAppBundleId.getId());
mobileAppBundleDao.removeById(tenantId, mobileAppBundleId.getId());
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(mobileAppBundleId).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 findMobileAppInfosByTenantId [{}]", tenantId);
PageData<MobileAppBundle> mobileBundles = mobileAppBundleDao.findByTenantId(tenantId, pageLink);
return mobileBundles.mapData(this::getMobileAppBundleInfo);
}
@Override
public MobileAppBundleInfo findMobileAppBundleInfoById(TenantId tenantId, MobileAppBundleId mobileAppIdBundle) {
log.trace("Executing findMobileAppInfoById [{}] [{}]", tenantId, mobileAppIdBundle);
MobileAppBundle mobileAppBundle = mobileAppBundleDao.findById(tenantId, mobileAppIdBundle.getId());
if (mobileAppBundle == null) {
return null;
}
return getMobileAppBundleInfo(mobileAppBundle);
}
@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(client);
}
for (MobileAppBundleOauth2Client client : newClientList) {
mobileAppBundleDao.addOauth2Client(client);
}
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId)
.entityId(mobileAppBundleId).created(false).build());
}
@Override
public MobileAppBundle findMobileAppBundleByPkgNameAndPlatform(TenantId tenantId, String pkgName, PlatformType platform) {
log.trace("Executing findMobileAppBundle, tenantId [{}], pkgName [{}], platform [{}]", tenantId, pkgName, platform);
return mobileAppBundleDao.findByPkgNameAndPlatform(tenantId, pkgName, platform);
}
@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 void deleteMobileAppsByTenantId(TenantId tenantId) {
log.trace("Executing deleteMobileAppsByTenantId, tenantId [{}]", tenantId);
mobileAppBundleDao.deleteByTenantId(tenantId);
}
@Override
public void deleteByTenantId(TenantId tenantId) {
deleteMobileAppsByTenantId(tenantId);
}
private MobileAppBundleInfo getMobileAppBundleInfo(MobileAppBundle mobileAppBundle) {
List<OAuth2ClientInfo> clients = oauth2ClientDao.findByMobileAppBundleId(mobileAppBundle.getUuidId()).stream()
.map(OAuth2ClientInfo::new)
.sorted(Comparator.comparing(OAuth2ClientInfo::getTitle))
.collect(Collectors.toList());
MobileApp androidApp = mobileAppService.findMobileAppById(mobileAppBundle.getTenantId(), mobileAppBundle.getAndroidAppId());
MobileApp iosApp = mobileAppService.findMobileAppById(mobileAppBundle.getTenantId(), mobileAppBundle.getIosAppId());
return new MobileAppBundleInfo(mobileAppBundle, androidApp != null ? androidApp.getPkgName() : null,
iosApp != null ? iosApp.getPkgName() : null, clients);
}
@Override
public EntityType getEntityType() {
return EntityType.MOBILE_APP_BUNDLE;
}
}

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

@ -15,26 +15,19 @@
*/
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.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, PageLink pageLink);
void deleteByTenantId(TenantId tenantId);
}

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

@ -19,36 +19,30 @@ 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.common.util.JacksonUtil;
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.AndroidQrCodeConfig;
import org.thingsboard.server.common.data.mobile.IosQrCodeConfig;
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.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 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;
@Autowired
private MobileAppDao mobileAppDao;
@ -80,43 +74,9 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil
}
@Override
public PageData<MobileAppInfo> findMobileAppInfosByTenantId(TenantId tenantId, PageLink pageLink) {
public PageData<MobileApp> findMobileAppsByTenantId(TenantId tenantId, 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, pageLink);
}
@Override
@ -137,16 +97,22 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil
}
@Override
public void deleteByTenantId(TenantId tenantId) {
deleteMobileAppsByTenantId(tenantId);
public AndroidQrCodeConfig findAndroidQrCodeConfig(TenantId tenantId, MobileAppBundleId mobileAppBundleId) {
log.trace("Executing findAndroidQrConfig, tenantId [{}], mobileAppBundleId [{}]", tenantId, mobileAppBundleId);
MobileApp mobileApp = mobileAppDao.findByBundleIdAndPlatformType(tenantId, mobileAppBundleId, PlatformType.ANDROID);
return mobileApp != null ? JacksonUtil.convertValue(mobileApp.getQrCodeConfig(), AndroidQrCodeConfig.class) : null;
}
@Override
public IosQrCodeConfig findIosQrCodeConfig(TenantId tenantId, MobileAppBundleId mobileAppBundleId) {
log.trace("Executing findAndroidQrConfig, tenantId [{}], mobileAppBundleId [{}]", tenantId, mobileAppBundleId);
MobileApp mobileApp = mobileAppDao.findByBundleIdAndPlatformType(tenantId, mobileAppBundleId, PlatformType.IOS);
return mobileApp != null ? JacksonUtil.convertValue(mobileApp.getQrCodeConfig(), IosQrCodeConfig.class) : null;
}
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) {
deleteMobileAppsByTenantId(tenantId);
}
@Override

14
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,19 @@
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.AndroidQrCodeConfig;
import org.thingsboard.server.common.data.mobile.IosQrCodeConfig;
import org.thingsboard.server.common.data.mobile.QrCodeSettings;
public interface MobileAppSettingsService {
public interface QrCodeSettingService {
MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings settings);
QrCodeSettings saveQrCodeSettings(TenantId tenantId, QrCodeSettings settings);
MobileAppSettings getMobileAppSettings(TenantId tenantId);
QrCodeSettings getQrCodeSettings(TenantId tenantId);
AndroidQrCodeConfig getAndroidQrCodeConfig(TenantId sysTenantId);
IosQrCodeConfig getIosQrCodeConfig(TenantId sysTenantId);
void deleteByTenantId(TenantId tenantId);

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

@ -0,0 +1,128 @@
/**
* 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.AndroidQrCodeConfig;
import org.thingsboard.server.common.data.mobile.BadgePosition;
import org.thingsboard.server.common.data.mobile.IosQrCodeConfig;
import org.thingsboard.server.common.data.mobile.QrCodeSettings;
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 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(
"mobile_app_settings_tenant_id_unq_key", "Mobile application for specified tenant already exists!"
));
throw e;
}
}
@Override
public QrCodeSettings getQrCodeSettings(TenantId tenantId) {
log.trace("Executing getMobileAppSettings for tenant [{}] ", tenantId);
QrCodeSettings qrCodeSettings = cache.getAndPutInTransaction(tenantId,
() -> qrCodeSettingsDao.findByTenantId(tenantId), true);
return constructMobileAppSettings(qrCodeSettings);
}
@Override
public AndroidQrCodeConfig getAndroidQrCodeConfig(TenantId tenantId) {
log.trace("Executing getAndroidAppConfig for tenant [{}] ", tenantId);
QrCodeSettings qrCodeSettings = getQrCodeSettings(tenantId);
return qrCodeSettings.getMobileAppBundleId() != null ? mobileAppService.findAndroidQrCodeConfig(tenantId, qrCodeSettings.getMobileAppBundleId()) : null;
}
@Override
public IosQrCodeConfig getIosQrCodeConfig(TenantId tenantId) {
log.trace("Executing getIOSAppConfig for tenant [{}] ", tenantId);
QrCodeSettings qrCodeSettings = getQrCodeSettings(tenantId);
return qrCodeSettings.getMobileAppBundleId() != null ? mobileAppService.findIosQrCodeConfig(tenantId, qrCodeSettings.getMobileAppBundleId()) : 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);
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.setDefaultGooglePlayLink(googlePlayLink);
qrCodeSettings.setDefaultAppStoreLink(appStoreLink);
}
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;
@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;
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;
@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));
}
}

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

@ -448,11 +448,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_QR_CODE_CONFIG_PROPERTY = "qr_code_config";
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_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";
/**
@ -684,11 +698,10 @@ 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_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};

107
dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java

@ -0,0 +1,107 @@
/**
* 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 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.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.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)
@Entity
@Table(name = ModelConstants.MOBILE_APP_BUNDLE_TABLE_NAME)
public class MobileAppBundleEntity extends BaseSqlEntity<MobileAppBundle> {
@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 MobileAppBundleEntity() {
super();
}
public MobileAppBundleEntity(MobileAppBundle mobile) {
super(mobile);
if (mobile.getTenantId() != null) {
this.tenantId = mobile.getTenantId().getId();
}
this.title = mobile.getTitle();
this.description = mobile.getDescription();
if (mobile.getAndroidAppId() != null) {
this.androidAppId = mobile.getAndroidAppId().getId();
}
if (mobile.getIosAppId() != null) {
this.iosAppID = mobile.getIosAppId().getId();
}
this.layoutConfig = toJson(mobile.getLayoutConfig());
this.oauth2Enabled = mobile.getOauth2Enabled();
}
@Override
public MobileAppBundle toData() {
MobileAppBundle mobileAppBundle = new MobileAppBundle();
mobileAppBundle.setId(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;
}
}

33
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.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();
public MobileAppBundleOauth2ClientEntity(MobileAppBundleOauth2Client domainOauth2Provider) {
mobileAppBundleId = domainOauth2Provider.getMobileAppBundleId().getId();
oauth2ClientId = domainOauth2Provider.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;
}

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

@ -15,16 +15,24 @@
*/
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.MobileAppStatus;
import org.thingsboard.server.common.data.mobile.MobileApp;
import org.thingsboard.server.common.data.mobile.QrCodeConfig;
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 +53,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_QR_CODE_CONFIG_PROPERTY)
private JsonNode qrCodeConfig;
public MobileAppEntity() {
super();
@ -59,7 +80,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 = mobile.getVersionInfo();
this.qrCodeConfig = toJson(mobile.getQrCodeConfig());
}
@Override
@ -72,7 +96,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(versionInfo);
mobile.setQrCodeConfig(fromJson(qrCodeConfig, QrCodeConfig.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;
}
}

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

@ -0,0 +1,81 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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;
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.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_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();
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);
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);

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!");
}
}
}

42
dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.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.dao.service.validator;
import lombok.AllArgsConstructor;
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.QRCodeConfig;
import org.thingsboard.server.common.data.mobile.QrCodeSettings;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
@Component
@AllArgsConstructor
public class QrCodeSettingsDataValidator extends DataValidator<QrCodeSettings> {
@Override
protected void validateDataImpl(TenantId tenantId, QrCodeSettings qrCodeSettings) {
MobileAppBundleId mobileAppBundleId = qrCodeSettings.getMobileAppBundleId();
QRCodeConfig qrCodeConfig = qrCodeSettings.getQrCodeConfig();
if (!qrCodeSettings.isUseDefaultApp() && (mobileAppBundleId == null)) {
throw new DataValidationException("Mobile app bundle is required to use custom application!");
}
if (qrCodeConfig == null) {
throw new DataValidationException("Qr code configuration is required!");
}
}
}

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

@ -0,0 +1,95 @@
/**
* 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.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.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.MobileAppOauth2ClientCompositeKey;
import org.thingsboard.server.dao.model.sql.MobileAppBundleOauth2ClientEntity;
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<MobileAppBundle> findByTenantId(TenantId tenantId, PageLink pageLink) {
return DaoUtil.toPageData(mobileAppBundleRepository.findByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink)));
}
@Override
public List<MobileAppBundleOauth2Client> findOauth2ClientsByMobileAppBundleId(TenantId tenantId, MobileAppBundleId mobileAppBundleId) {
return DaoUtil.convertDataList(mobileOauth2ProviderRepository.findAllByMobileAppBundleId(mobileAppBundleId.getId()));
}
@Override
public void addOauth2Client(MobileAppBundleOauth2Client mobileAppBundleOauth2Client) {
mobileOauth2ProviderRepository.save(new MobileAppBundleOauth2ClientEntity(mobileAppBundleOauth2Client));
}
@Override
public void removeOauth2Client(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.name()));
}
@Override
public void deleteByTenantId(TenantId tenantId) {
mobileAppBundleRepository.deleteByTenantId(tenantId.getId());
}
@Override
public EntityType getEntityType() {
return EntityType.MOBILE_APP_BUNDLE;
}
}

34
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.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,24 +51,20 @@ 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)));
}
@Override
public List<MobileAppOauth2Client> findOauth2ClientsByMobileAppId(TenantId tenantId, MobileAppId mobileAppId) {
return DaoUtil.convertDataList(mobileOauth2ProviderRepository.findAllByMobileAppId(mobileAppId.getId()));
public MobileApp findByBundleIdAndPlatformType(TenantId tenantId, MobileAppBundleId mobileAppBundleId, PlatformType platformType) {
switch (platformType) {
case ANDROID:
return DaoUtil.getData(mobileAppRepository.findAndroidAppByBundleId(mobileAppBundleId.getId()));
case IOS:
return DaoUtil.getData(mobileAppRepository.findIOSAppByBundleId(mobileAppBundleId.getId()));
default:
return null;
}
}
@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 PageData<MobileApp> findByTenantId(TenantId tenantId, PageLink pageLink) {
return DaoUtil.toPageData(mobileAppRepository.findByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink)));
}
@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;
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);
}

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

@ -0,0 +1,51 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.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.dao.model.sql.MobileAppBundleEntity;
import org.thingsboard.server.dao.model.sql.OAuth2ClientEntity;
import java.util.List;
import java.util.UUID;
public interface MobileAppBundleRepository extends JpaRepository<MobileAppBundleEntity, UUID> {
@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") String 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);
}

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

@ -39,4 +39,10 @@ public interface MobileAppRepository extends JpaRepository<MobileAppEntity, UUID
@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

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

@ -46,9 +46,10 @@ 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 a on (b.androidAppId = a.id or b.iosAppID = a.id) " +
"WHERE a.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);
@ -61,16 +62,17 @@ public interface OAuth2ClientRepository extends JpaRepository<OAuth2ClientEntity
@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, EntityType.OAUTH2_CLIENT, EntityType.MOBILE_APP_BUNDLE
);
}

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

@ -634,7 +634,22 @@ CREATE TABLE IF NOT EXISTS mobile_app (
tenant_id uuid,
pkg_name varchar(255) UNIQUE,
app_secret varchar(2048),
oauth2_enabled boolean
platform_type varchar(32),
status varchar(32),
version_info varchar(16384),
qr_code_config varchar(16384)
);
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),
android_app_id uuid,
ios_app_id uuid,
description varchar(1024),
layout_config varchar(16384),
oauth2_enabled boolean
);
CREATE TABLE IF NOT EXISTS domain_oauth2_client (
@ -644,10 +659,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
);
@ -886,13 +901,12 @@ 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),
mobile_app_bundle_id uuid,
qr_code_config VARCHAR(100000),
CONSTRAINT mobile_app_settings_tenant_id_unq_key UNIQUE (tenant_id)
);

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

@ -21,21 +21,15 @@ 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.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 {
@ -74,43 +68,22 @@ 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 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, 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) {
MobileApp MobileApp = new MobileApp();
MobileApp.setTenantId(tenantId);
MobileApp.setPkgName(mobileAppName);
MobileApp.setAppSecret(StringUtils.randomAlphanumeric(24));
MobileApp.setOauth2Enabled(oauth2Enabled);
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

51
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;
@ -124,7 +125,7 @@ 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.MobileAppBundle;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo;
@ -2171,19 +2172,19 @@ public class RestClient implements Closeable {
restTemplate.postForLocation(baseURL + "/api/domain/{id}/oauth2Clients", oauth2ClientIds, domainId.getId());
}
public List<DomainInfo> getTenantMobileAppInfos() {
public List<DomainInfo> getTenantMobileApps() {
return restTemplate.exchange(
baseURL + "/api/mobileApp/infos",
baseURL + "/api/mobile/app",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<DomainInfo>>() {
}).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 +2195,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 List<DomainInfo> getTenantMobileBundleInfos() {
return restTemplate.exchange(
baseURL + "/api/mobile/bundle/infos",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<DomainInfo>>() {
}).getBody();
}
public Optional<MobileAppBundle> getMobileBundleById(MobileAppBundleId mobileAppBundleId) {
try {
ResponseEntity<MobileAppBundle> mobileApp = restTemplate.getForEntity(baseURL + "/api/mobile/app/{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() {

Loading…
Cancel
Save