From 04bba3ff5acb2ff703574556d1611b6df93aea16 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 7 Oct 2024 14:26:33 +0300 Subject: [PATCH 01/48] mobile app bundles: initial implementation --- .../server/controller/BaseController.java | 50 ++++ .../controller/DashboardController.java | 36 +-- .../controller/MobileAppBundleController.java | 132 +++++++++ .../controller/MobileAppController.java | 43 +-- .../server/controller/MobileV2Controller.java | 65 +++++ ...ler.java => QrCodeSettingsController.java} | 60 ++-- .../controller/SystemInfoController.java | 10 +- .../DefaultTbMobileAppBundleService.java | 83 ++++++ .../mobile/DefaultTbMobileAppService.java | 18 +- .../mobile/TbMobileAppBundleService.java | 32 +++ .../entitiy/mobile/TbMobileAppService.java | 7 +- .../service/security/permission/Resource.java | 1 + .../permission/SysAdminPermissions.java | 1 + .../src/main/resources/thingsboard.yml | 2 +- .../MobileAppBundleControllerTest.java | 149 ++++++++++ .../controller/MobileAppControllerTest.java | 107 ++++---- .../MobileApplicationControllerTest.java | 253 ----------------- .../QrCodeSettingsControllerTest.java | 258 ++++++++++++++++++ .../dao/mobile/MobileAppBundleService.java | 47 ++++ .../server/dao/mobile/MobileAppService.java | 14 +- .../server/common/data/CacheConstants.java | 2 +- .../server/common/data/EntityType.java | 3 +- .../common/data/id/EntityIdFactory.java | 2 + .../common/data/id/MobileAppBundleId.java | 39 +++ ...pSettingsId.java => QrCodeSettingsId.java} | 8 +- ...idConfig.java => AndroidQrCodeConfig.java} | 11 +- .../{IosConfig.java => IosQrCodeConfig.java} | 10 +- .../server/common/data/mobile/MobileApp.java | 22 +- .../common/data/mobile/MobileAppBundle.java | 84 ++++++ ...eAppInfo.java => MobileAppBundleInfo.java} | 18 +- ....java => MobileAppBundleOauth2Client.java} | 6 +- .../common/data/mobile/MobileAppStatus.java | 24 ++ .../data/mobile/MobileLayoutConfig.java | 40 +++ .../common/data/mobile/MobileLoginInfo.java | 28 ++ .../common/data/mobile/MobileMenuItem.java | 41 +++ .../common/data/mobile/MobileMenuPath.java | 32 +++ .../common/data/mobile/MobileUserInfo.java | 29 ++ .../common/data/mobile/QrCodeConfig.java | 37 +++ ...leAppSettings.java => QrCodeSettings.java} | 18 +- .../mobile/BaseMobileAppSettingsService.java | 120 -------- .../server/dao/mobile/MobileAppBundleDao.java | 43 +++ .../mobile/MobileAppBundleServiceImpl.java | 170 ++++++++++++ .../server/dao/mobile/MobileAppDao.java | 15 +- .../dao/mobile/MobileAppServiceImpl.java | 76 ++---- ...Service.java => QrCodeSettingService.java} | 14 +- .../dao/mobile/QrCodeSettingServiceImpl.java | 128 +++++++++ ....java => QrCodeSettingsCaffeineCache.java} | 10 +- ...ettingsDao.java => QrCodeSettingsDao.java} | 6 +- ...ent.java => QrCodeSettingsEvictEvent.java} | 2 +- ...che.java => QrCodeSettingsRedisCache.java} | 10 +- .../server/dao/model/ModelConstants.java | 31 ++- .../dao/model/sql/MobileAppBundleEntity.java | 107 ++++++++ ...=> MobileAppBundleOauth2ClientEntity.java} | 33 +-- .../server/dao/model/sql/MobileAppEntity.java | 35 ++- .../MobileAppOauth2ClientCompositeKey.java | 2 +- .../model/sql/MobileAppSettingsEntity.java | 85 ------ .../dao/model/sql/QrCodeSettingsEntity.java | 81 ++++++ .../server/dao/oauth2/OAuth2ClientDao.java | 2 +- .../MobileAppSettingsDataValidator.java | 51 ---- .../QrCodeSettingsDataValidator.java | 42 +++ .../dao/sql/mobile/JpaMobileAppBundleDao.java | 95 +++++++ .../dao/sql/mobile/JpaMobileAppDao.java | 34 +-- ...ingsDao.java => JpaQrCodeSettingsDao.java} | 24 +- ...obileAppBundleOauth2ClientRepository.java} | 6 +- .../sql/mobile/MobileAppBundleRepository.java | 51 ++++ .../dao/sql/mobile/MobileAppRepository.java | 6 + ...ory.java => QrCodeSettingsRepository.java} | 8 +- .../dao/sql/oauth2/JpaOAuth2ClientDao.java | 4 +- .../sql/oauth2/OAuth2ClientRepository.java | 26 +- .../server/dao/tenant/TenantServiceImpl.java | 8 +- .../main/resources/sql/schema-entities.sql | 28 +- .../dao/service/MobileAppServiceTest.java | 35 +-- .../resources/application-test.properties | 4 +- .../thingsboard/rest/client/RestClient.java | 51 +++- 74 files changed, 2316 insertions(+), 949 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java create mode 100644 application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java rename application/src/main/java/org/thingsboard/server/controller/{MobileApplicationController.java => QrCodeSettingsController.java} (75%) create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppBundleService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppBundleService.java create mode 100644 application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java delete mode 100644 application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java create mode 100644 application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java create mode 100644 common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java rename common/data/src/main/java/org/thingsboard/server/common/data/id/{MobileAppSettingsId.java => QrCodeSettingsId.java} (77%) rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{AndroidConfig.java => AndroidQrCodeConfig.java} (78%) rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{IosConfig.java => IosQrCodeConfig.java} (78%) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundle.java rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{MobileAppInfo.java => MobileAppBundleInfo.java} (63%) rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{MobileAppOauth2Client.java => MobileAppBundleOauth2Client.java} (85%) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppStatus.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLayoutConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLoginInfo.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuItem.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuPath.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileUserInfo.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeConfig.java rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{MobileAppSettings.java => QrCodeSettings.java} (76%) delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java rename dao/src/main/java/org/thingsboard/server/dao/mobile/{MobileAppSettingsService.java => QrCodeSettingService.java} (59%) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java rename dao/src/main/java/org/thingsboard/server/dao/mobile/{MobileAppSettingsCaffeineCache.java => QrCodeSettingsCaffeineCache.java} (76%) rename dao/src/main/java/org/thingsboard/server/dao/mobile/{MobileAppSettingsDao.java => QrCodeSettingsDao.java} (80%) rename dao/src/main/java/org/thingsboard/server/dao/mobile/{MobileAppSettingsEvictEvent.java => QrCodeSettingsEvictEvent.java} (94%) rename dao/src/main/java/org/thingsboard/server/dao/mobile/{MobileAppSettingsRedisCache.java => QrCodeSettingsRedisCache.java} (71%) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java rename dao/src/main/java/org/thingsboard/server/dao/model/sql/{MobileAppOauth2ClientEntity.java => MobileAppBundleOauth2ClientEntity.java} (57%) delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/QrCodeSettingsEntity.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java rename dao/src/main/java/org/thingsboard/server/dao/sql/mobile/{JpaMobileAppSettingsDao.java => JpaQrCodeSettingsDao.java} (59%) rename dao/src/main/java/org/thingsboard/server/dao/sql/mobile/{MobileAppOauth2ClientRepository.java => MobileAppBundleOauth2ClientRepository.java} (72%) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java rename dao/src/main/java/org/thingsboard/server/dao/sql/mobile/{MobileAppSettingsRepository.java => QrCodeSettingsRepository.java} (76%) diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 9b7616d3fa..d2f0884ba6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/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 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); diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 2949df35a7..4183fc45e5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/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()) { diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java new file mode 100644 index 0000000000..f54314bb9a --- /dev/null +++ b/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 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 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()); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java index fba17d7119..b19267d44a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java +++ b/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 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 getTenantMobileAppInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @GetMapping(value = "/mobile/app") + public PageData 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); diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java new file mode 100644 index 0000000000..1f8cb1da77 --- /dev/null +++ b/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 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); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java similarity index 75% rename from application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java rename to application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java index 11f017b85f..1de38b5671 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java +++ b/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 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 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(); diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 9600ac0221..18d632345b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/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; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppBundleService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppBundleService.java new file mode 100644 index 0000000000..a8c20a772b --- /dev/null +++ b/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 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 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; + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java index 8dbef1fc1e..be0d4640a0 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java +++ b/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 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 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) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppBundleService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppBundleService.java new file mode 100644 index 0000000000..d523f6a764 --- /dev/null +++ b/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 oauth2Clients, User user) throws Exception; + + void updateOauth2Clients(MobileAppBundle mobileAppBundle, List oAuth2ClientIds, User user); + + void delete(MobileAppBundle mobileAppBundle, User user); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java index bcc9cf7004..97255e16cc 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java +++ b/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 oauth2Clients, User user) throws Exception; - - void updateOauth2Clients(MobileApp mobileApp, List oAuth2ClientIds, User user); + MobileApp save(MobileApp mobileApp, User user) throws Exception; void delete(MobileApp mobileApp, User user); diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index 16a3c4be59..8c6f4d00e0 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/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), diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index 4790a07965..8edbd9f6d5 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/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); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index e9c8668958..4eca2e93ed 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/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: diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java new file mode 100644 index 0000000000..26c1a207e2 --- /dev/null +++ b/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> PAGE_DATA_MOBILE_APP_BUNDLE_TYPE_REF = new TypeReference<>() { + }; + static final TypeReference> PAGE_DATA_MOBILE_APP_TYPE_REF = new TypeReference<>() { + }; + static final TypeReference> 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 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 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 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; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java index 6fd2d62d14..e4f9ab583a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java +++ b/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> PAGE_DATA_MOBILE_APP_TYPE_REF = new TypeReference<>() { - }; - static final TypeReference> PAGE_DATA_OAUTH2_CLIENT_TYPE_REF = new TypeReference<>() { + static final TypeReference> 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 pageData = doGetTypedWithPageLink("/api/mobileApp/infos?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0)); + PageData 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 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 pageData = doGetTypedWithPageLink("/api/mobileApp/infos?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0)); + PageData 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 pageData2 = doGetTypedWithPageLink("/api/mobileApp/infos?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0)); + PageData 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; } diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java deleted file mode 100644 index 39e2ef6a40..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java +++ /dev/null @@ -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"); - } -} diff --git a/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java new file mode 100644 index 0000000000..2ed4933d2b --- /dev/null +++ b/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> PAGE_DATA_MOBILE_APP_BUNDLE_TYPE_REF = new TypeReference<>() { + }; + static final TypeReference> 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 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 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; + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java new file mode 100644 index 0000000000..45e404d4af --- /dev/null +++ b/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 findMobileAppBundleInfosByTenantId(TenantId tenantId, PageLink pageLink); + + MobileAppBundleInfo findMobileAppBundleInfoById(TenantId tenantId, MobileAppBundleId mobileAppBundleId); + + void updateOauth2Clients(TenantId tenantId, MobileAppBundleId mobileAppBundleId, List oAuth2ClientIds); + + MobileAppBundle findMobileAppBundleByPkgNameAndPlatform(TenantId tenantId, String pkgName, PlatformType platform); + + void deleteMobileAppsByTenantId(TenantId tenantId); +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java index 2976022116..57be235577 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java +++ b/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 findMobileAppInfosByTenantId(TenantId tenantId, PageLink pageLink); + PageData findMobileAppsByTenantId(TenantId tenantId, PageLink pageLink); - MobileAppInfo findMobileAppInfoById(TenantId tenantId, MobileAppId mobileAppId); + AndroidQrCodeConfig findAndroidQrCodeConfig(TenantId tenantId, MobileAppBundleId mobileAppBundleId); - void updateOauth2Clients(TenantId tenantId, MobileAppId mobileAppId, List oAuth2ClientIds); + IosQrCodeConfig findIosQrCodeConfig(TenantId tenantId, MobileAppBundleId mobileAppBundleId); void deleteMobileAppsByTenantId(TenantId tenantId); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 54a5bc755d..71c52d43c1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -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"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index b5ce79e20f..89b434047b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/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 diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index 5a85e6ce67..8e2688248e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/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!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java new file mode 100644 index 0000000000..b3344dbcef --- /dev/null +++ b/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; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QrCodeSettingsId.java similarity index 77% rename from common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java rename to common/data/src/main/java/org/thingsboard/server/common/data/id/QrCodeSettingsId.java index d152701fc4..b58e1a98a3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/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)); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidQrCodeConfig.java similarity index 78% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidQrCodeConfig.java index d670382462..01e4845702 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/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; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosQrCodeConfig.java similarity index 78% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosQrCodeConfig.java index 7d40dfe805..84ed6c726e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/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; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java index ad207e5635..018d79b965 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java +++ b/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 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 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 diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundle.java new file mode 100644 index 0000000000..10c8637f35 --- /dev/null +++ b/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 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; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleInfo.java similarity index 63% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppInfo.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleInfo.java index 4d85028e36..64f3396d56 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppInfo.java +++ b/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 oauth2ClientInfos; - public MobileAppInfo(MobileApp mobileApp, List oauth2ClientInfos) { + public MobileAppBundleInfo(MobileAppBundle mobileApp, String androidPkgName, String iosPkgName, List 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); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppOauth2Client.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleOauth2Client.java similarity index 85% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppOauth2Client.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleOauth2Client.java index 2be75db92f..fcd261ead1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppOauth2Client.java +++ b/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; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppStatus.java new file mode 100644 index 0000000000..f1043c732d --- /dev/null +++ b/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 + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLayoutConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLayoutConfig.java new file mode 100644 index 0000000000..662ceee13f --- /dev/null +++ b/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 items = new ArrayList<>(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLoginInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLoginInfo.java new file mode 100644 index 0000000000..2d9717f279 --- /dev/null +++ b/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 oAuth2ClientLoginInfos; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuItem.java new file mode 100644 index 0000000000..ef876f88d9 --- /dev/null +++ b/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; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuPath.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuPath.java new file mode 100644 index 0000000000..68d81c146a --- /dev/null +++ b/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 +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileUserInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileUserInfo.java new file mode 100644 index 0000000000..1d078ab6d1 --- /dev/null +++ b/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; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeConfig.java new file mode 100644 index 0000000000..01f4eaa98a --- /dev/null +++ b/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(); + + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeSettings.java similarity index 76% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeSettings.java index 31b2029bfc..ed2578cc88 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/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 implements HasTenantId { +public class QrCodeSettings extends BaseData implements HasTenantId { private static final long serialVersionUID = 2628323657987010348L; @@ -36,12 +38,8 @@ public class MobileAppSettings extends BaseData 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 implements @JsonProperty(access = JsonProperty.Access.READ_ONLY) private String defaultAppStoreLink; - public MobileAppSettings() { + public QrCodeSettings() { } - public MobileAppSettings(MobileAppSettingsId id) { + public QrCodeSettings(QrCodeSettingsId id) { super(id); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java deleted file mode 100644 index 5ec717da09..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java +++ /dev/null @@ -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 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 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; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.java new file mode 100644 index 0000000000..b376a0096f --- /dev/null +++ b/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 { + + PageData findByTenantId(TenantId tenantId, PageLink pageLink); + + List 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); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java new file mode 100644 index 0000000000..b67ad2cac3 --- /dev/null +++ b/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 findMobileAppBundleInfosByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findMobileAppInfosByTenantId [{}]", tenantId); + PageData 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 oAuth2ClientIds) { + log.trace("Executing updateOauth2Clients, mobileAppId [{}], oAuth2ClientIds [{}]", mobileAppBundleId, oAuth2ClientIds); + Set newClientList = oAuth2ClientIds.stream() + .map(clientId -> new MobileAppBundleOauth2Client(mobileAppBundleId, clientId)) + .collect(Collectors.toSet()); + + List existingClients = mobileAppBundleDao.findOauth2ClientsByMobileAppBundleId(tenantId, mobileAppBundleId); + List 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> 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 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; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java index 8da92e1eb3..091e61327c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java +++ b/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 { - PageData findByTenantId(TenantId tenantId, PageLink pageLink); - - List findOauth2ClientsByMobileAppId(TenantId tenantId, MobileAppId mobileAppId); + MobileApp findByBundleIdAndPlatformType(TenantId tenantId, MobileAppBundleId mobileAppBundleId, PlatformType platformType); - void addOauth2Client(MobileAppOauth2Client mobileAppOauth2Client); - - void removeOauth2Client(MobileAppOauth2Client mobileAppOauth2Client); + PageData findByTenantId(TenantId tenantId, PageLink pageLink); void deleteByTenantId(TenantId tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java index 66431a38dc..2b96d88497 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java +++ b/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 findMobileAppInfosByTenantId(TenantId tenantId, PageLink pageLink) { + public PageData findMobileAppsByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findMobileAppInfosByTenantId [{}]", tenantId); - PageData 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 oAuth2ClientIds) { - log.trace("Executing updateOauth2Clients, mobileAppId [{}], oAuth2ClientIds [{}]", mobileAppId, oAuth2ClientIds); - Set newClientList = oAuth2ClientIds.stream() - .map(clientId -> new MobileAppOauth2Client(mobileAppId, clientId)) - .collect(Collectors.toSet()); - - List existingClients = mobileAppDao.findOauth2ClientsByMobileAppId(tenantId, mobileAppId); - List 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 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 diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java similarity index 59% rename from dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java rename to dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java index 4645947382..ae46e4039b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java +++ b/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); diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java new file mode 100644 index 0000000000..bda099bcc0 --- /dev/null +++ b/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 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 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; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsCaffeineCache.java similarity index 76% rename from dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java rename to dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsCaffeineCache.java index 9e15ae8572..806d7af0a9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java +++ b/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 { +@Service("QrCodeSettingsCache") +public class QrCodeSettingsCaffeineCache extends CaffeineTbTransactionalCache { - public MobileAppSettingsCaffeineCache(CacheManager cacheManager) { - super(cacheManager, CacheConstants.MOBILE_APP_SETTINGS_CACHE); + public QrCodeSettingsCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.QR_CODE_SETTINGS_CACHE); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsDao.java similarity index 80% rename from dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java rename to dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsDao.java index 094ead1a5b..8b3600ceea 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java +++ b/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 { +public interface QrCodeSettingsDao extends Dao { - MobileAppSettings findByTenantId(TenantId tenantId); + QrCodeSettings findByTenantId(TenantId tenantId); void removeByTenantId(TenantId tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsEvictEvent.java similarity index 94% rename from dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java rename to dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsEvictEvent.java index 459e61ef6d..b9b4f84576 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java +++ b/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; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsRedisCache.java similarity index 71% rename from dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java rename to dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsRedisCache.java index aac802c1ec..063c79c2e7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java +++ b/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 { +@Service("QrCodeSettingsCache") +public class QrCodeSettingsRedisCache extends RedisTbTransactionalCache { - 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)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index b920160cc0..20fdf88ebf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -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}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java new file mode 100644 index 0000000000..030d995203 --- /dev/null +++ b/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 { + + @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; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleOauth2ClientEntity.java similarity index 57% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientEntity.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleOauth2ClientEntity.java index b2c7a55855..58040d27d9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientEntity.java +++ b/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 { +public final class MobileAppBundleOauth2ClientEntity implements ToData { @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; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java index 0313864d09..69d5fe4e6e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java +++ b/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 { @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 { } 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 { 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; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientCompositeKey.java index b372751549..843735f9eb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientCompositeKey.java +++ b/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; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java deleted file mode 100644 index cd38edd9cb..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java +++ /dev/null @@ -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 { - - @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; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/QrCodeSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QrCodeSettingsEntity.java new file mode 100644 index 0000000000..55c301cbd0 --- /dev/null +++ b/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 { + + @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; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java index cf40a331fb..01d56feb20 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java @@ -36,7 +36,7 @@ public interface OAuth2ClientDao extends Dao { List findByDomainId(UUID domainId); - List findByMobileAppId(UUID mobileAppId); + List findByMobileAppBundleId(UUID mobileAppBundleId); String findAppSecret(UUID id, String pkgName); diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java deleted file mode 100644 index c867b08cc4..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java +++ /dev/null @@ -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 { - - @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!"); - } - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java new file mode 100644 index 0000000000..073b5eb53e --- /dev/null +++ b/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 { + + @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!"); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java new file mode 100644 index 0000000000..01ac158863 --- /dev/null +++ b/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 implements MobileAppBundleDao { + + private final MobileAppBundleRepository mobileAppBundleRepository; + private final MobileAppBundleOauth2ClientRepository mobileOauth2ProviderRepository; + + @Override + protected Class getEntityClass() { + return MobileAppBundleEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return mobileAppBundleRepository; + } + + @Override + public PageData findByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(mobileAppBundleRepository.findByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); + } + + @Override + public List 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; + } + +} + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java index 0453481d08..61b34955e9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java @@ -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 implements MobileAppDao { private final MobileAppRepository mobileAppRepository; - private final MobileAppOauth2ClientRepository mobileOauth2ProviderRepository; @Override protected Class getEntityClass() { @@ -55,24 +51,20 @@ public class JpaMobileAppDao extends JpaAbstractDao } @Override - public PageData findByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(mobileAppRepository.findByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); - } - - @Override - public List 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 findByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(mobileAppRepository.findByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaQrCodeSettingsDao.java similarity index 59% rename from dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppSettingsDao.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaQrCodeSettingsDao.java index e4286bb949..b415af445c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppSettingsDao.java +++ b/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 implements MobileAppSettingsDao { +public class JpaQrCodeSettingsDao extends JpaAbstractDao 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 getEntityClass() { - return MobileAppSettingsEntity.class; + protected Class getEntityClass() { + return QrCodeSettingsEntity.class; } @Override - protected JpaRepository getRepository() { - return mobileAppSettingsRepository; + protected JpaRepository getRepository() { + return qrCodeSettingsRepository; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppOauth2ClientRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleOauth2ClientRepository.java similarity index 72% rename from dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppOauth2ClientRepository.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleOauth2ClientRepository.java index 9abf9b3b61..bc865dea4c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppOauth2ClientRepository.java +++ b/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 { +public interface MobileAppBundleOauth2ClientRepository extends JpaRepository { - List findAllByMobileAppId(UUID mobileAppId); + List findAllByMobileAppBundleId(UUID mobileAppId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java new file mode 100644 index 0000000000..e935d9b246 --- /dev/null +++ b/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 { + + @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 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); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppRepository.java index 185a277644..56aa8dfc9f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppRepository.java @@ -39,4 +39,10 @@ public interface MobileAppRepository extends JpaRepository { +public interface QrCodeSettingsRepository extends JpaRepository { - 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); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java index 4a585186d9..d694ca0f93 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java @@ -75,8 +75,8 @@ public class JpaOAuth2ClientDao extends JpaAbstractDao findByMobileAppId(UUID mobileAppId) { - return DaoUtil.convertDataList(repository.findByMobileAppId(mobileAppId)); + public List findByMobileAppBundleId(UUID mobileAppBundleId) { + return DaoUtil.convertDataList(repository.findByMobileAppBundleId(mobileAppBundleId)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java index 6805ca06ee..e6a1485a87 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java @@ -46,9 +46,10 @@ public interface OAuth2ClientRepository extends JpaRepository findEnabledByPkgNameAndPlatformType(@Param("pkgName") String pkgName, @Param("platformFilter") String platformFilter); @@ -61,16 +62,17 @@ public interface OAuth2ClientRepository extends JpaRepository findByMobileAppId(@Param("mobileAppId") UUID mobileAppId); + "LEFT JOIN MobileAppBundleOauth2ClientEntity bc on bc.oauth2ClientId = c.id " + + "WHERE bc.mobileAppBundleId = :mobileAppBundleId ") + List 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); diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 8e6c38814a..9d550ff341 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/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 MobileApps = new ArrayList<>(); + List 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 retrieved = mobileAppService.findMobileAppInfosByTenantId(TenantId.SYS_TENANT_ID, new PageLink(10, 0)); - List MobileAppInfos = MobileApps.stream().map(MobileApp -> new MobileAppInfo(MobileApp, Collections.emptyList())).toList(); - assertThat(retrieved.getData()).containsOnlyOnceElementsOf(MobileAppInfos); + PageData 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 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 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; } } diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index e71c6b1294..1e11c77f9a 100644 --- a/dao/src/test/resources/application-test.properties +++ b/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 diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 818d714a3b..c1e6d3d7e0 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/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 getTenantMobileAppInfos() { + public List getTenantMobileApps() { return restTemplate.exchange( - baseURL + "/api/mobileApp/infos", + baseURL + "/api/mobile/app", HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { }).getBody(); } - public Optional getMobileAppInfoById(MobileAppId mobileAppId) { + public Optional getMobileAppById(MobileAppId mobileAppId) { try { - ResponseEntity mobileAppInfo = restTemplate.getForEntity(baseURL + "/api/mobileApp/info/{id}", MobileAppInfo.class, mobileAppId.getId()); - return Optional.ofNullable(mobileAppInfo.getBody()); + ResponseEntity 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 getTenantMobileBundleInfos() { + return restTemplate.exchange( + baseURL + "/api/mobile/bundle/infos", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public Optional getMobileBundleById(MobileAppBundleId mobileAppBundleId) { + try { + ResponseEntity 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() { From cc64a27050772332b48c3f03dc85547fd89d0a7f Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 8 Oct 2024 14:58:25 +0300 Subject: [PATCH 02/48] upgrade script, optimized queries to db, refactoring --- .../main/data/upgrade/3.8.0/schema_update.sql | 129 ++++++++++++++++++ .../controller/MobileAppBundleController.java | 2 +- .../controller/MobileAppController.java | 22 +-- .../server/controller/MobileV2Controller.java | 12 +- .../controller/QrCodeSettingsController.java | 23 +++- .../MobileAppBundleControllerTest.java | 30 ++-- .../QrCodeSettingsControllerTest.java | 20 +-- .../dao/mobile/MobileAppBundleService.java | 2 +- .../server/dao/mobile/MobileAppService.java | 9 +- ...ileLoginInfo.java => LoginMobileInfo.java} | 7 +- .../server/common/data/mobile/MobileApp.java | 7 +- .../data/mobile/MobileAppBundleInfo.java | 6 + .../data/mobile/MobileAppVersionInfo.java | 49 +++++++ .../common/data/mobile/QrCodeConfig.java | 1 + .../common/data/mobile/UserMobileInfo.java | 11 +- ...erInfo.java => UserMobileSessionInfo.java} | 14 +- .../server/dao/mobile/MobileAppBundleDao.java | 9 +- .../mobile/MobileAppBundleServiceImpl.java | 56 ++++---- .../dao/mobile/MobileAppServiceImpl.java | 12 +- .../dao/mobile/QrCodeSettingService.java | 8 +- .../dao/mobile/QrCodeSettingServiceImpl.java | 17 +-- .../server/dao/model/ModelConstants.java | 2 +- .../sql/AbstractMobileAppBundleEntity.java | 114 ++++++++++++++++ .../dao/model/sql/MobileAppBundleEntity.java | 75 +--------- .../model/sql/MobileAppBundleInfoEntity.java | 43 ++++++ .../server/dao/model/sql/MobileAppEntity.java | 5 +- .../dao/sql/mobile/JpaMobileAppBundleDao.java | 16 ++- .../sql/mobile/MobileAppBundleRepository.java | 24 +++- .../sql/oauth2/OAuth2ClientRepository.java | 22 +-- .../server/dao/user/UserServiceImpl.java | 12 +- .../main/resources/sql/schema-entities.sql | 22 +-- 31 files changed, 539 insertions(+), 242 deletions(-) rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{MobileLoginInfo.java => LoginMobileInfo.java} (82%) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppVersionInfo.java rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{MobileUserInfo.java => UserMobileSessionInfo.java} (70%) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractMobileAppBundleEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleInfoEntity.java diff --git a/application/src/main/data/upgrade/3.8.0/schema_update.sql b/application/src/main/data/upgrade/3.8.0/schema_update.sql index 6b87dc6dde..f9fd1e9181 100644 --- a/application/src/main/data/upgrade/3.8.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.0/schema_update.sql @@ -14,3 +14,132 @@ -- limitations under the License. -- +-- CREATE MOBILE APP BUNDLES FROM EXISTING APPS + +CREATE TABLE IF NOT EXISTS mobile_app_bundle ( + id uuid NOT NULL CONSTRAINT mobile_app_bundle_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid, + title varchar(255), + android_app_id uuid, + ios_app_id uuid, + description varchar(1024), + layout_config varchar(16384), + oauth2_enabled boolean, + CONSTRAINT android_app_id_unq_key UNIQUE (android_app_id), + CONSTRAINT ios_app_id_unq_key UNIQUE (ios_app_id), + CONSTRAINT fk_android_app_id FOREIGN KEY (android_app_id) REFERENCES mobile_app(id), + CONSTRAINT fk_ios_app_id FOREIGN KEY (ios_app_id) REFERENCES mobile_app(id) +); + +ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS platform_type varchar(32), + ADD COLUMN IF NOT EXISTS status varchar(32), + ADD COLUMN IF NOT EXISTS version_info varchar(16384), + ADD COLUMN IF NOT EXISTS qr_code_config varchar(16384), + DROP CONSTRAINT IF EXISTS mobile_app_pkg_name_key; + +-- rename mobile_app_oauth2_client to mobile_app_bundle_oauth2_client +DO +$$ + BEGIN + -- in case of running the upgrade script a second time + IF EXISTS(SELECT * FROM information_schema.tables WHERE table_name='mobile_app_oauth2_client') THEN + ALTER TABLE mobile_app_oauth2_client RENAME TO mobile_app_bundle_oauth2_client; + ALTER TABLE mobile_app_bundle_oauth2_client DROP CONSTRAINT IF EXISTS fk_domain; + ALTER TABLE mobile_app_bundle_oauth2_client RENAME COLUMN mobile_app_id TO mobile_app_bundle_id; + END IF; + END; +$$; + + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- duplicate each mobile app and create mobile app bundle for the pair of android and ios app +DO +$$ + DECLARE + generatedBundleId uuid; + iosAppId uuid; + mobileAppRecord RECORD; + BEGIN + -- in case of running the upgrade script a second time + IF EXISTS(SELECT * FROM information_schema.columns WHERE table_name='mobile_app' and column_name='oauth2_enabled') THEN + UPDATE mobile_app SET platform_type = 'ANDROID' WHERE platform_type IS NULL; + UPDATE mobile_app SET status = 'PUBLISHED' WHERE mobile_app.status IS NULL; + FOR mobileAppRecord IN SELECT * FROM mobile_app + LOOP + -- duplicate app for iOS platform type + iosAppId := uuid_generate_v4(); + INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, app_secret, platform_type, status) + VALUES (iosAppId, (extract(epoch from now()) * 1000), mobileAppRecord.tenant_id, mobileAppRecord.pkg_name, mobileAppRecord.app_secret, 'IOS', mobileAppRecord.status); + -- create bundle for android and iOS app + generatedBundleId := uuid_generate_v4(); + INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id, ios_app_id, oauth2_enabled) + VALUES (generatedBundleId, (extract(epoch from now()) * 1000), mobileAppRecord.tenant_id, + 'App bundle ' || mobileAppRecord.pkg_name, mobileAppRecord.id, iosAppId, mobileAppRecord.oauth2_enabled); + UPDATE mobile_app_bundle_oauth2_client SET mobile_app_bundle_id = generatedBundleId WHERE mobile_app_bundle_id = mobileAppRecord.id; + END LOOP; + END IF; + ALTER TABLE mobile_app DROP COLUMN IF EXISTS oauth2_enabled; + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'pkg_platform_unique') THEN + ALTER TABLE mobile_app ADD CONSTRAINT pkg_platform_unique UNIQUE (pkg_name, platform_type); + END IF; + END; +$$; + +ALTER TABLE IF EXISTS mobile_app_settings RENAME TO qr_code_settings; +ALTER TABLE qr_code_settings ADD COLUMN IF NOT EXISTS mobile_app_bundle_id uuid; + +-- migrate mobile apps from qr code settings to mobile_app, create mobile app bundle for the pair of apps +DO +$$ + DECLARE + iosPkgName varchar; + androidAppId uuid; + iosAppId uuid; + generatedBundleId uuid; + qrCodeRecord RECORD; + BEGIN + -- in case of running the upgrade script a second time + IF EXISTS(SELECT * FROM information_schema.columns WHERE table_name = 'qr_code_settings' and column_name = 'android_config') THEN + FOR qrCodeRecord IN SELECT * FROM qr_code_settings + LOOP + -- migrate android config + SELECT id into androidAppId FROM mobile_app WHERE pkg_name = qrCodeRecord.android_config::jsonb ->> 'appPackage' AND platform_type = 'ANDROID'; + IF androidAppId IS NULL THEN + androidAppId := uuid_generate_v4(); + INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, qr_code_config) + VALUES (androidAppId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, + qrCodeRecord.android_config::jsonb ->> 'appPackage', 'ANDROID', 'PUBLISHED', qrCodeRecord.android_config::jsonb || '{"type": "ANDROID"}'::jsonb); + generatedBundleId := uuid_generate_v4(); + INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id) + VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, 'App bundle for qr code', androidAppId); + UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId WHERE id = qrCodeRecord.id; + ELSE + UPDATE mobile_app SET qr_code_config = qrCodeRecord.android_config::jsonb || '{"type": "ANDROID"}'::jsonb WHERE id = androidAppId; + END IF; + + -- migrate ios config + iosPkgName := substring(qrCodeRecord.ios_config::jsonb ->> 'appId', strpos(qrCodeRecord.ios_config::jsonb ->> 'appId', '.') + 1); + SELECT id into iosAppId FROM mobile_app WHERE pkg_name = iosPkgName AND platform_type = 'IOS'; + IF iosAppId IS NULL THEN + iosAppId := uuid_generate_v4(); + INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, qr_code_config) + VALUES (iosAppId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, + iosPkgName, 'IOS', 'PUBLISHED', qrCodeRecord.ios_config::jsonb || '{"type": "IOS"}'::jsonb); + IF generatedBundleId IS NULL THEN + generatedBundleId := uuid_generate_v4(); + INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, ios_app_id) + VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, 'App bundle for qr code', iosAppId); + UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId WHERE id = qrCodeRecord.id; + ELSE + UPDATE mobile_app_bundle SET ios_app_id = iosAppId WHERE id = generatedBundleId; + END IF; + ELSE + UPDATE mobile_app SET qr_code_config = qrCodeRecord.ios_config::jsonb || '{"type": "IOS"}'::jsonb WHERE id = iosAppId; + END IF; + END LOOP; + END IF; + ALTER TABLE qr_code_settings DROP COLUMN IF EXISTS android_config, DROP COLUMN IF EXISTS ios_config; + END; +$$; diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java index f54314bb9a..0853cb9eb2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java @@ -106,7 +106,7 @@ public class MobileAppBundleController extends BaseController { @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); + accessControlService.checkPermission(getCurrentUser(), Resource.MOBILE_APP_BUNDLE, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return mobileAppBundleService.findMobileAppBundleInfosByTenantId(getTenantId(), pageLink); } diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java index b19267d44a..4fe2d4cd2a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java @@ -76,16 +76,16 @@ public class MobileAppController extends BaseController { @ApiOperation(value = "Get mobile app infos (getTenantMobileAppInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @GetMapping(value = "/mobile/app") - public PageData getTenantMobileAppInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) - @RequestParam int pageSize, - @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) - @RequestParam int page, - @Parameter(description = "Case-insensitive 'substring' filter based on app's name") - @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION) - @RequestParam(required = false) String sortProperty, - @Parameter(description = SORT_ORDER_DESCRIPTION) - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData getTenantMobileApps(@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 mobileAppService.findMobileAppsByTenantId(getTenantId(), pageLink); @@ -94,7 +94,7 @@ public class MobileAppController extends BaseController { @ApiOperation(value = "Get mobile info by id (getMobileAppInfoById)", notes = SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @GetMapping(value = "/mobile/app/{id}") - public MobileApp getMobileAppInfoById(@PathVariable UUID id) throws ThingsboardException { + public MobileApp getMobileAppById(@PathVariable UUID id) throws ThingsboardException { MobileAppId mobileAppId = new MobileAppId(id); return checkEntityId(mobileAppId, mobileAppService::findMobileAppById, Operation.READ); } diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java index 1f8cb1da77..b06d280b9a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java @@ -25,8 +25,8 @@ 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.mobile.LoginMobileInfo; +import org.thingsboard.server.common.data.mobile.UserMobileInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -40,17 +40,17 @@ import java.util.List; public class MobileV2Controller extends BaseController { @GetMapping(value = "/api/noauth/mobile") - public MobileLoginInfo getMobileUserLoginSettings(@Parameter(description = "Mobile application package name") + public LoginMobileInfo getMobileUserLoginSettings(@Parameter(description = "Mobile application package name") @RequestParam String pkgName, @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) @RequestParam PlatformType platform) { List oauth2Clients = oAuth2ClientService.findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(pkgName, platform); - return new MobileLoginInfo(oauth2Clients); + return new LoginMobileInfo(oauth2Clients); } @GetMapping(value = "/api/auth/mobile") - public MobileUserInfo getMobileUserSettings(@Parameter(description = "Mobile application package name") + public UserMobileInfo getMobileUserSettings(@Parameter(description = "Mobile application package name") @RequestParam String pkgName, @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) @@ -59,7 +59,7 @@ public class MobileV2Controller extends BaseController { 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); + return new UserMobileInfo(user, homeDashboardInfo, mobileAppBundle != null ? mobileAppBundle.getLayoutConfig() : null); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java index 1de38b5671..c1ce037a11 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java @@ -31,10 +31,13 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mobile.AndroidQrCodeConfig; import org.thingsboard.server.common.data.mobile.IosQrCodeConfig; +import org.thingsboard.server.common.data.mobile.MobileApp; import org.thingsboard.server.common.data.mobile.QrCodeSettings; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.mobile.MobileAppService; @@ -49,6 +52,8 @@ import org.thingsboard.server.service.security.system.SystemSecurityService; import java.net.URI; import java.net.URISyntaxException; +import static org.thingsboard.server.common.data.oauth2.PlatformType.ANDROID; +import static org.thingsboard.server.common.data.oauth2.PlatformType.IOS; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; @@ -96,7 +101,7 @@ public class QrCodeSettingsController extends BaseController { @ApiOperation(value = "Get associated android applications (getAssetLinks)") @GetMapping(value = "/.well-known/assetlinks.json") public ResponseEntity getAssetLinks() { - AndroidQrCodeConfig androidQrConfig = qrCodeSettingService.getAndroidQrCodeConfig(TenantId.SYS_TENANT_ID); + AndroidQrCodeConfig androidQrConfig = (AndroidQrCodeConfig) qrCodeSettingService.findAppQrCodeConfig(TenantId.SYS_TENANT_ID, ANDROID); if (androidQrConfig != null && androidQrConfig.isEnabled()) { return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, androidQrConfig.getAppPackage(), androidQrConfig.getSha256CertFingerprints()))); } else { @@ -107,7 +112,7 @@ public class QrCodeSettingsController extends BaseController { @ApiOperation(value = "Get associated ios applications (getAppleAppSiteAssociation)") @GetMapping(value = "/.well-known/apple-app-site-association") public ResponseEntity getAppleAppSiteAssociation() { - IosQrCodeConfig iosQrCodeConfig = qrCodeSettingService.getIosQrCodeConfig(TenantId.SYS_TENANT_ID); + IosQrCodeConfig iosQrCodeConfig = (IosQrCodeConfig) qrCodeSettingService.findAppQrCodeConfig(TenantId.SYS_TENANT_ID, IOS); if (iosQrCodeConfig != null && iosQrCodeConfig.isEnabled() && iosQrCodeConfig.getAppId() != null) { return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, iosQrCodeConfig.getAppId()))); } else { @@ -173,14 +178,12 @@ public class QrCodeSettingsController extends BaseController { 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(); + String googlePlayLink = useDefaultApp ? qrCodeSettings.getDefaultGooglePlayLink() : getStoreLink(qrCodeSettings.getMobileAppBundleId(), ANDROID); 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(); + String appStoreLink = useDefaultApp ? qrCodeSettings.getDefaultAppStoreLink() : getStoreLink(qrCodeSettings.getMobileAppBundleId(), IOS); return ResponseEntity.status(HttpStatus.FOUND) .header("Location", appStoreLink) .build(); @@ -189,4 +192,12 @@ public class QrCodeSettingsController extends BaseController { } } + private String getStoreLink(MobileAppBundleId mobileAppBundleId, PlatformType platformType) { + if (mobileAppBundleId == null) { + return null; + } + MobileApp mobileApp = mobileAppService.findByBundleIdAndPlatformType(TenantId.SYS_TENANT_ID, mobileAppBundleId, platformType); + return (mobileApp != null && mobileApp.getQrCodeConfig() != null) ? mobileApp.getQrCodeConfig().getStoreLink() : null; + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java index 26c1a207e2..b81ee82ecd 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java @@ -32,7 +32,10 @@ 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.Comparator; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -64,15 +67,15 @@ public class MobileAppBundleControllerTest extends AbstractControllerTest { @After public void tearDown() throws Exception { - PageData 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()) + PageData pageData2 = doGetTypedWithPageLink("/api/mobile/bundle/infos?", PAGE_DATA_MOBILE_APP_BUNDLE_TYPE_REF, new PageLink(10, 0)); + for (MobileAppBundleInfo appBundleInfo : pageData2.getData()) { + doDelete("/api/mobile/bundle/" + appBundleInfo.getId().getId()) .andExpect(status().isOk()); } - PageData 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()) + PageData 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()); } @@ -84,7 +87,7 @@ public class MobileAppBundleControllerTest extends AbstractControllerTest { } @Test - public void testSaveMobileApp() throws Exception { + public void testSaveMobileAppBundle() { MobileAppBundle mobileAppBundle = new MobileAppBundle(); mobileAppBundle.setTitle("Test bundle"); mobileAppBundle.setAndroidAppId(androidApp.getId()); @@ -97,7 +100,7 @@ public class MobileAppBundleControllerTest extends AbstractControllerTest { @Test - public void testUpdateMobileAppOauth2Clients() throws Exception { + public void testUpdateMobileAppBundleOauth2Clients() throws Exception { MobileAppBundle mobileAppBundle = new MobileAppBundle(); mobileAppBundle.setTitle("Test bundle"); mobileAppBundle.setAndroidAppId(androidApp.getId()); @@ -113,11 +116,14 @@ public class MobileAppBundleControllerTest extends AbstractControllerTest { 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)))); + MobileAppBundleInfo retrievedMobileAppBundleInfo = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedAppBundle.getId().getId()); + assertThat(retrievedMobileAppBundleInfo).isEqualTo(new MobileAppBundleInfo(savedAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), + Stream.of(new OAuth2ClientInfo(savedOAuth2Client), new OAuth2ClientInfo(savedOAuth2Client2)) + .sorted(Comparator.comparing(OAuth2ClientInfo::getTitle)).collect(Collectors.toList()) + )); doPut("/api/mobile/bundle/" + savedAppBundle.getId() + "/oauth2Clients", List.of(savedOAuth2Client2.getId().getId())); - MobileAppBundleInfo retrievedMobileAppInfo2 = doGet("/api/mobileApp/info/{id}", MobileAppBundleInfo.class, savedOAuth2Client.getId().getId()); + MobileAppBundleInfo retrievedMobileAppInfo2 = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedAppBundle.getId().getId()); assertThat(retrievedMobileAppInfo2).isEqualTo(new MobileAppBundleInfo(savedAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), List.of(new OAuth2ClientInfo(savedOAuth2Client2)))); } @@ -133,7 +139,7 @@ public class MobileAppBundleControllerTest extends AbstractControllerTest { 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()); + MobileAppBundleInfo retrievedMobileAppInfo = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedMobileAppBundle.getId().getId()); assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppBundleInfo(savedMobileAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), List.of(new OAuth2ClientInfo(savedOAuth2Client)))); } diff --git a/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java index 2ed4933d2b..88949a7e11 100644 --- a/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java @@ -107,17 +107,17 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { @After public void tearDown() throws Exception { - PageData 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()); - } - + loginSysAdmin(); PageData pageData2 = doGetTypedWithPageLink("/api/mobile/bundle/infos?", PAGE_DATA_MOBILE_APP_BUNDLE_TYPE_REF, new PageLink(10, 0)); for (MobileAppBundleInfo appBundleInfo : pageData2.getData()) { doDelete("/api/mobile/bundle/" + appBundleInfo.getId().getId()) .andExpect(status().isOk()); } + PageData pageData = doGetTypedWithPageLink("/api/mobile/app?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0)); + for (MobileApp mobileApp : pageData.getData()) { + doDelete("/api/mobile/app/" + mobileApp.getId().getId()) + .andExpect(status().isOk()); + } } @Test @@ -148,7 +148,7 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { doPost("/api/qr/settings", qrCodeSettings) .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Bundle required to use custom application!"))); + .andExpect(statusReason(containsString("Mobile app bundle is required to use custom application!"))); qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId()); doPost("/api/qr/settings", qrCodeSettings) @@ -175,7 +175,8 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { public void testGetApplicationAssociations() throws Exception { loginSysAdmin(); QrCodeSettings qrCodeSettings = doGet("/api/qr/settings", QrCodeSettings.class); - qrCodeSettings.setUseDefaultApp(false); + qrCodeSettings.setUseDefaultApp(true); + qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId()); doPost("/api/qr/settings", qrCodeSettings) .andExpect(status().isOk()); @@ -226,7 +227,8 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { loginSysAdmin(); QrCodeSettings qrCodeSettings = doGet("/api/qr/settings", QrCodeSettings.class); qrCodeSettings.setUseDefaultApp(false); - doPost("/api/mobile/app/settings", qrCodeSettings); + qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId()); + doPost("/api/qr/settings", qrCodeSettings); String customAppDeepLink = doGet("/api/qr/deepLink", String.class); Pattern customAppExpectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)\""); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java index 45e404d4af..ba1c7ae3ef 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java @@ -43,5 +43,5 @@ public interface MobileAppBundleService extends EntityDaoService { MobileAppBundle findMobileAppBundleByPkgNameAndPlatform(TenantId tenantId, String pkgName, PlatformType platform); - void deleteMobileAppsByTenantId(TenantId tenantId); + void deleteMobileAppBundlesByTenantId(TenantId tenantId); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java index 57be235577..01ebe81d30 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java @@ -18,9 +18,8 @@ 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.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.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; @@ -29,15 +28,13 @@ public interface MobileAppService extends EntityDaoService { MobileApp saveMobileApp(TenantId tenantId, MobileApp mobileApp); - void deleteMobileAppById(TenantId tenantId, MobileAppId mobileAppId); - MobileApp findMobileAppById(TenantId tenantId, MobileAppId mobileAppId); PageData findMobileAppsByTenantId(TenantId tenantId, PageLink pageLink); - AndroidQrCodeConfig findAndroidQrCodeConfig(TenantId tenantId, MobileAppBundleId mobileAppBundleId); + MobileApp findByBundleIdAndPlatformType(TenantId tenantId, MobileAppBundleId mobileAppBundleId, PlatformType platformType); - IosQrCodeConfig findIosQrCodeConfig(TenantId tenantId, MobileAppBundleId mobileAppBundleId); + void deleteMobileAppById(TenantId tenantId, MobileAppId mobileAppId); void deleteMobileAppsByTenantId(TenantId tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLoginInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/LoginMobileInfo.java similarity index 82% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLoginInfo.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/LoginMobileInfo.java index 2d9717f279..f146291123 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLoginInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/LoginMobileInfo.java @@ -15,14 +15,9 @@ */ 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 oAuth2ClientLoginInfos; +public record LoginMobileInfo(List oAuth2ClientLoginInfos) { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java index 018d79b965..5e28815908 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java @@ -16,7 +16,6 @@ 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; @@ -31,7 +30,6 @@ 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 @@ -53,9 +51,8 @@ public class MobileApp extends BaseData implements HasTenantId, Has @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; + @Valid + private MobileAppVersionInfo versionInfo; @Schema(description = "Application qr code configuration") @Valid private QrCodeConfig qrCodeConfig; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleInfo.java index 64f3396d56..ec076c1007 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleInfo.java @@ -35,6 +35,12 @@ public class MobileAppBundleInfo extends MobileAppBundle { @Schema(description = "List of available oauth2 clients") private List oauth2ClientInfos; + public MobileAppBundleInfo(MobileAppBundle mobileApp, String androidPkgName, String iosPkgName) { + super(mobileApp); + this.androidPkgName = androidPkgName; + this.iosPkgName = iosPkgName; + } + public MobileAppBundleInfo(MobileAppBundle mobileApp, String androidPkgName, String iosPkgName, List oauth2ClientInfos) { super(mobileApp); this.androidPkgName = androidPkgName; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppVersionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppVersionInfo.java new file mode 100644 index 0000000000..948955f315 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppVersionInfo.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.validation.Length; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class MobileAppVersionInfo { + + @Schema(description = "Minimum supported version") + @Length(fieldName = "minVersion", max = 20) + private String minVersion; + + @Schema(description = "Release notes of minimum supported version") + @Length(fieldName = "minVersionReleaseNotes", max = 10000) + private String minVersionReleaseNotes; + + @Schema(description = "Latest supported version") + @Length(fieldName = "latestVersion", max = 20) + private String latestVersion; + + @Schema(description = "Release notes of latest supported version") + @Length(fieldName = "latestVersionReleaseNotes", max = 10000) + private String latestVersionReleaseNotes; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeConfig.java index 01f4eaa98a..167b2c6a91 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeConfig.java @@ -33,5 +33,6 @@ public interface QrCodeConfig { PlatformType getType(); + String getStoreLink(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java index 21f72dd4db..671e9ec271 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java @@ -15,13 +15,8 @@ */ package org.thingsboard.server.common.data.mobile; -import lombok.Data; - -import java.util.Map; - -@Data -public class UserMobileInfo { - - private Map sessions; +import org.thingsboard.server.common.data.HomeDashboardInfo; +import org.thingsboard.server.common.data.User; +public record UserMobileInfo(User user, HomeDashboardInfo homeDashboardInfo, MobileLayoutConfig layoutConfig) { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileUserInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileSessionInfo.java similarity index 70% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileUserInfo.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileSessionInfo.java index 1d078ab6d1..414059a92c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileUserInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileSessionInfo.java @@ -15,15 +15,13 @@ */ 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; + +import java.util.Map; @Data -@AllArgsConstructor -public class MobileUserInfo { - User user; - HomeDashboardInfo homeDashboardInfo; - MobileLayoutConfig layoutConfig; +public class UserMobileSessionInfo { + + private Map sessions; + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.java index b376a0096f..7b801a3c30 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.java @@ -18,6 +18,7 @@ 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.MobileAppBundleInfo; import org.thingsboard.server.common.data.mobile.MobileAppBundleOauth2Client; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; @@ -28,13 +29,15 @@ import java.util.List; public interface MobileAppBundleDao extends Dao { - PageData findByTenantId(TenantId tenantId, PageLink pageLink); + PageData findInfosByTenantId(TenantId tenantId, PageLink pageLink); + + MobileAppBundleInfo findInfoById(TenantId tenantId, MobileAppBundleId mobileAppBundleId); List findOauth2ClientsByMobileAppBundleId(TenantId tenantId, MobileAppBundleId mobileAppBundleId); - void addOauth2Client(MobileAppBundleOauth2Client mobileAppBundleOauth2Client); + void addOauth2Client(TenantId tenantId, MobileAppBundleOauth2Client mobileAppBundleOauth2Client); - void removeOauth2Client(MobileAppBundleOauth2Client mobileAppBundleOauth2Client); + void removeOauth2Client(TenantId tenantId, MobileAppBundleOauth2Client mobileAppBundleOauth2Client); MobileAppBundle findByPkgNameAndPlatform(TenantId tenantId, String pkgName, PlatformType platform); diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java index b67ad2cac3..388e9cb623 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java @@ -25,7 +25,6 @@ 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; @@ -53,8 +52,6 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements private OAuth2ClientDao oauth2ClientDao; @Autowired private MobileAppBundleDao mobileAppBundleDao; - @Autowired - private MobileAppService mobileAppService; @Override public MobileAppBundle saveMobileAppBundle(TenantId tenantId, MobileAppBundle mobileApp) { @@ -73,9 +70,18 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements @Override public void deleteMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId) { - log.trace("Executing deleteMobileAppById [{}]", mobileAppBundleId.getId()); + log.trace("Executing deleteMobileAppBundleById [{}]", mobileAppBundleId.getId()); mobileAppBundleDao.removeById(tenantId, mobileAppBundleId.getId()); eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(mobileAppBundleId).build()); + + try { + mobileAppBundleDao.removeById(tenantId, mobileAppBundleId.getId()); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(mobileAppBundleId).build()); + } catch (Exception e) { + checkConstraintViolation(e, "fk_android_app_id", "The mobile app referenced by the mobile bundle cannot be deleted!", + "fk_ios_app_id", "The mobile app referenced by the mobile bundle cannot be deleted!"); + throw e; + } } @Override @@ -86,19 +92,20 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements @Override public PageData findMobileAppBundleInfosByTenantId(TenantId tenantId, PageLink pageLink) { - log.trace("Executing findMobileAppInfosByTenantId [{}]", tenantId); - PageData mobileBundles = mobileAppBundleDao.findByTenantId(tenantId, pageLink); - return mobileBundles.mapData(this::getMobileAppBundleInfo); + log.trace("Executing findMobileAppBundleInfosByTenantId [{}]", tenantId); + PageData mobileBundles = mobileAppBundleDao.findInfosByTenantId(tenantId, pageLink); + mobileBundles.getData().forEach(this::fetchOauth2Clients); + return mobileBundles; } @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; + MobileAppBundleInfo mobileAppBundleInfo = mobileAppBundleDao.findInfoById(tenantId, mobileAppIdBundle); + if (mobileAppBundleInfo != null) { + fetchOauth2Clients(mobileAppBundleInfo); } - return getMobileAppBundleInfo(mobileAppBundle); + return mobileAppBundleInfo; } @Override @@ -115,10 +122,10 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements newClientList.removeIf(existingClients::contains); for (MobileAppBundleOauth2Client client : toRemoveList) { - mobileAppBundleDao.removeOauth2Client(client); + mobileAppBundleDao.removeOauth2Client(tenantId, client); } for (MobileAppBundleOauth2Client client : newClientList) { - mobileAppBundleDao.addOauth2Client(client); + mobileAppBundleDao.addOauth2Client(tenantId, client); } eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId) .entityId(mobileAppBundleId).created(false).build()); @@ -142,29 +149,26 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements } @Override - public void deleteMobileAppsByTenantId(TenantId tenantId) { + public void deleteMobileAppBundlesByTenantId(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 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); + deleteMobileAppBundlesByTenantId(tenantId); } @Override public EntityType getEntityType() { return EntityType.MOBILE_APP_BUNDLE; } + + private void fetchOauth2Clients(MobileAppBundleInfo mobileAppBundleInfo) { + List clients = oauth2ClientDao.findByMobileAppBundleId(mobileAppBundleInfo.getUuidId()).stream() + .map(OAuth2ClientInfo::new) + .sorted(Comparator.comparing(OAuth2ClientInfo::getTitle)) + .collect(Collectors.toList()); + mobileAppBundleInfo.setOauth2ClientInfos(clients); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java index 2b96d88497..a471b30589 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java @@ -97,17 +97,9 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil } @Override - public AndroidQrCodeConfig findAndroidQrCodeConfig(TenantId tenantId, MobileAppBundleId mobileAppBundleId) { + public MobileApp findByBundleIdAndPlatformType(TenantId tenantId, MobileAppBundleId mobileAppBundleId, PlatformType platformType) { 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; + return mobileAppDao.findByBundleIdAndPlatformType(tenantId, mobileAppBundleId, platformType); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java index ae46e4039b..48d221af9d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java @@ -16,9 +16,9 @@ package org.thingsboard.server.dao.mobile; 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.QrCodeConfig; import org.thingsboard.server.common.data.mobile.QrCodeSettings; +import org.thingsboard.server.common.data.oauth2.PlatformType; public interface QrCodeSettingService { @@ -26,9 +26,7 @@ public interface QrCodeSettingService { QrCodeSettings getQrCodeSettings(TenantId tenantId); - AndroidQrCodeConfig getAndroidQrCodeConfig(TenantId sysTenantId); - - IosQrCodeConfig getIosQrCodeConfig(TenantId sysTenantId); + QrCodeConfig findAppQrCodeConfig(TenantId sysTenantId, PlatformType platformType); void deleteByTenantId(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java index bda099bcc0..5e6ae2fcef 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java @@ -21,11 +21,11 @@ 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.QrCodeConfig; 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.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.service.DataValidator; @@ -75,17 +75,10 @@ public class QrCodeSettingServiceImpl extends AbstractCachedEntityService extends BaseSqlEntity { + + @Column(name = TENANT_ID_COLUMN) + private UUID tenantId; + + @Column(name = ModelConstants.MOBILE_APP_BUNDLE_TITLE_PROPERTY) + private String title; + + @Column(name = ModelConstants.MOBILE_APP_BUNDLE_DESCRIPTION_PROPERTY) + private String description; + + @Column(name = ModelConstants.MOBILE_APP_BUNDLE_ANDROID_APP_ID_PROPERTY) + private UUID androidAppId; + + @Column(name = ModelConstants.MOBILE_APP_BUNDLE_IOS_APP_ID_PROPERTY) + private UUID iosAppID; + + @Convert(converter = JsonConverter.class) + @Column(name = ModelConstants.MOBILE_APP_BUNDLE_LAYOUT_CONFIG_PROPERTY) + private JsonNode layoutConfig; + + @Column(name = ModelConstants.MOBILE_APP_BUNDLE_OAUTH2_ENABLED_PROPERTY) + private Boolean oauth2Enabled; + + public AbstractMobileAppBundleEntity() { + super(); + } + + public AbstractMobileAppBundleEntity(MobileAppBundleEntity mobileAppBundleEntity) { + super(mobileAppBundleEntity); + this.tenantId = mobileAppBundleEntity.getTenantId(); + this.title = mobileAppBundleEntity.getTitle(); + this.description = mobileAppBundleEntity.getDescription(); + this.androidAppId = mobileAppBundleEntity.getAndroidAppId(); + this.iosAppID = mobileAppBundleEntity.getIosAppID(); + this.layoutConfig = mobileAppBundleEntity.getLayoutConfig(); + this.oauth2Enabled = mobileAppBundleEntity.getOauth2Enabled(); + } + + public AbstractMobileAppBundleEntity(T mobileAppBundle) { + super(mobileAppBundle); + if (mobileAppBundle.getTenantId() != null) { + this.tenantId = mobileAppBundle.getTenantId().getId(); + } + this.title = mobileAppBundle.getTitle(); + this.description = mobileAppBundle.getDescription(); + if (mobileAppBundle.getAndroidAppId() != null) { + this.androidAppId = mobileAppBundle.getAndroidAppId().getId(); + } + if (mobileAppBundle.getIosAppId() != null) { + this.iosAppID = mobileAppBundle.getIosAppId().getId(); + } + this.layoutConfig = toJson(mobileAppBundle.getLayoutConfig()); + this.oauth2Enabled = mobileAppBundle.getOauth2Enabled(); + } + + protected MobileAppBundle toMobileAppBundle() { + MobileAppBundle mobileAppBundle = new MobileAppBundle(new MobileAppBundleId(id)); + mobileAppBundle.setCreatedTime(createdTime); + mobileAppBundle.setTitle(title); + mobileAppBundle.setDescription(description); + if (tenantId != null) { + mobileAppBundle.setTenantId(TenantId.fromUUID(tenantId)); + } + if (androidAppId != null) { + mobileAppBundle.setAndroidAppId(new MobileAppId(androidAppId)); + } + if (iosAppID != null) { + mobileAppBundle.setIosAppId(new MobileAppId(iosAppID)); + } + mobileAppBundle.setLayoutConfig(fromJson(layoutConfig, MobileLayoutConfig.class)); + mobileAppBundle.setOauth2Enabled(oauth2Enabled); + return mobileAppBundle; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java index 030d995203..66b5a98457 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java @@ -15,93 +15,30 @@ */ 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; +import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_TABLE_NAME; @Data @EqualsAndHashCode(callSuper = true) @Entity -@Table(name = ModelConstants.MOBILE_APP_BUNDLE_TABLE_NAME) -public class MobileAppBundleEntity extends BaseSqlEntity { - - @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; +@Table(name = MOBILE_APP_BUNDLE_TABLE_NAME) +public final class MobileAppBundleEntity extends AbstractMobileAppBundleEntity { 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(); + public MobileAppBundleEntity(MobileAppBundle mobileAppBundle) { + super(mobileAppBundle); } @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; + return super.toMobileAppBundle(); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleInfoEntity.java new file mode 100644 index 0000000000..9248e4f722 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleInfoEntity.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.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.mobile.MobileAppBundleInfo; + +@Data +@EqualsAndHashCode(callSuper = true) +public class MobileAppBundleInfoEntity extends AbstractMobileAppBundleEntity { + + private String androidPkgName; + private String iosPkgName; + + public MobileAppBundleInfoEntity() { + super(); + } + + public MobileAppBundleInfoEntity(MobileAppBundleEntity mobileAppBundleEntity, String androidPkgName, String iosPkgName) { + super(mobileAppBundleEntity); + this.androidPkgName = androidPkgName; + this.iosPkgName = iosPkgName; + } + + @Override + public MobileAppBundleInfo toData() { + return new MobileAppBundleInfo(super.toMobileAppBundle(), androidPkgName, iosPkgName); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java index 69d5fe4e6e..8cd42b8d4a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java @@ -28,6 +28,7 @@ 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.MobileAppVersionInfo; import org.thingsboard.server.common.data.mobile.QrCodeConfig; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.dao.model.BaseSqlEntity; @@ -82,7 +83,7 @@ public class MobileAppEntity extends BaseSqlEntity { this.appSecret = mobile.getAppSecret(); this.platformType = mobile.getPlatformType(); this.status = mobile.getStatus(); - this.versionInfo = mobile.getVersionInfo(); + this.versionInfo = toJson(mobile.getVersionInfo()); this.qrCodeConfig = toJson(mobile.getQrCodeConfig()); } @@ -98,7 +99,7 @@ public class MobileAppEntity extends BaseSqlEntity { mobile.setAppSecret(appSecret); mobile.setPlatformType(platformType); mobile.setStatus(status); - mobile.setVersionInfo(versionInfo); + mobile.setVersionInfo(fromJson(versionInfo, MobileAppVersionInfo.class)); mobile.setQrCodeConfig(fromJson(qrCodeConfig, QrCodeConfig.class)); return mobile; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java index 01ac158863..0d9bc0dbf5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java @@ -22,6 +22,7 @@ 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.MobileAppBundleInfo; import org.thingsboard.server.common.data.mobile.MobileAppBundleOauth2Client; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; @@ -29,8 +30,8 @@ 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.model.sql.MobileAppOauth2ClientCompositeKey; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; @@ -56,8 +57,13 @@ public class JpaMobileAppBundleDao extends JpaAbstractDao findByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(mobileAppBundleRepository.findByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); + public PageData findInfosByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(mobileAppBundleRepository.findInfoByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); + } + + @Override + public MobileAppBundleInfo findInfoById(TenantId tenantId, MobileAppBundleId mobileAppBundleId) { + return DaoUtil.getData(mobileAppBundleRepository.findInfoById(mobileAppBundleId.getId())); } @Override @@ -66,12 +72,12 @@ public class JpaMobileAppBundleDao extends JpaAbstractDao { + @Query("SELECT new org.thingsboard.server.dao.model.sql.MobileAppBundleInfoEntity(b, andApp.pkgName, iosApp.pkgName) " + + "FROM MobileAppBundleEntity b " + + "LEFT JOIN MobileAppEntity andApp on b.androidAppId = andApp.id " + + "LEFT JOIN MobileAppEntity iosApp on b.iosAppID = iosApp.id " + + "WHERE b.tenantId = :tenantId AND " + + "(:searchText is NULL OR ilike(b.title, concat('%', :searchText, '%')) = true)") + Page findInfoByTenantId(@Param("tenantId") UUID tenantId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.MobileAppBundleInfoEntity(b, andApp.pkgName, iosApp.pkgName) " + + "FROM MobileAppBundleEntity b " + + "LEFT JOIN MobileAppEntity andApp on b.androidAppId = andApp.id " + + "LEFT JOIN MobileAppEntity iosApp on b.iosAppID = iosApp.id " + + "WHERE b.id = :bundleId ") + MobileAppBundleInfoEntity findInfoById(UUID bundleId); + @Query("SELECT b " + "FROM MobileAppBundleEntity b " + "LEFT JOIN MobileAppEntity a on b.androidAppId = a.id or b.iosAppID = a.id " + @@ -40,8 +56,8 @@ public interface MobileAppBundleRepository extends JpaRepository findByTenantId(@Param("tenantId") UUID tenantId, - @Param("searchText") String searchText, - Pageable pageable); + @Param("searchText") String searchText, + Pageable pageable); @Transactional @Modifying diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java index e6a1485a87..788644504a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java @@ -37,8 +37,8 @@ public interface OAuth2ClientRepository extends JpaRepository findEnabledByDomainNameAndPlatformType(@Param("domainName") String domainName, @@ -46,31 +46,31 @@ public interface OAuth2ClientRepository extends JpaRepository findEnabledByPkgNameAndPlatformType(@Param("pkgName") String pkgName, @Param("platformFilter") String platformFilter); @Query("SELECT c " + "FROM OAuth2ClientEntity c " + - "LEFT JOIN DomainOauth2ClientEntity dc on dc.oauth2ClientId = c.id " + + "LEFT JOIN DomainOauth2ClientEntity dc ON dc.oauth2ClientId = c.id " + "WHERE dc.domainId = :domainId ") List findByDomainId(@Param("domainId") UUID domainId); @Query("SELECT c " + "FROM OAuth2ClientEntity c " + - "LEFT JOIN MobileAppBundleOauth2ClientEntity bc on bc.oauth2ClientId = c.id " + + "LEFT JOIN MobileAppBundleOauth2ClientEntity bc ON bc.oauth2ClientId = c.id " + "WHERE bc.mobileAppBundleId = :mobileAppBundleId ") List findByMobileAppBundleId(@Param("mobileAppBundleId") UUID mobileAppBundleId); @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 " + + "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, diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index ba0d3d69f3..9cc55169a9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -46,7 +46,7 @@ import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.mobile.MobileSessionInfo; -import org.thingsboard.server.common.data.mobile.UserMobileInfo; +import org.thingsboard.server.common.data.mobile.UserMobileSessionInfo; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; @@ -468,8 +468,8 @@ public class UserServiceImpl extends AbstractCachedEntityService { - UserMobileInfo newMobileInfo = new UserMobileInfo(); + UserMobileSessionInfo mobileInfo = findMobileInfo(tenantId, userId).orElseGet(() -> { + UserMobileSessionInfo newMobileInfo = new UserMobileSessionInfo(); newMobileInfo.setSessions(new HashMap<>()); return newMobileInfo; }); @@ -479,7 +479,7 @@ public class UserServiceImpl extends AbstractCachedEntityService findMobileSessions(TenantId tenantId, UserId userId) { - return findMobileInfo(tenantId, userId).map(UserMobileInfo::getSessions).orElse(Collections.emptyMap()); + return findMobileInfo(tenantId, userId).map(UserMobileSessionInfo::getSessions).orElse(Collections.emptyMap()); } @Override @@ -495,9 +495,9 @@ public class UserServiceImpl extends AbstractCachedEntityService findMobileInfo(TenantId tenantId, UserId userId) { + private Optional findMobileInfo(TenantId tenantId, UserId userId) { return Optional.ofNullable(userSettingsService.findUserSettings(tenantId, userId, UserSettingsType.MOBILE)) - .map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileInfo.class)); + .map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileSessionInfo.class)); } @Override diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 164645c249..762e4b6670 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -641,15 +641,19 @@ CREATE TABLE IF NOT EXISTS mobile_app ( ); 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 + 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, + CONSTRAINT android_app_id_unq_key UNIQUE (android_app_id), + CONSTRAINT ios_app_id_unq_key UNIQUE (ios_app_id), + CONSTRAINT fk_android_app_id FOREIGN KEY (android_app_id) REFERENCES mobile_app(id), + CONSTRAINT fk_ios_app_id FOREIGN KEY (ios_app_id) REFERENCES mobile_app(id) ); CREATE TABLE IF NOT EXISTS domain_oauth2_client ( From 44cd542601ea96884b9bff0e56b14bb31bd8c776 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 8 Oct 2024 17:57:46 +0300 Subject: [PATCH 03/48] upgrade script fix --- .../main/data/upgrade/3.8.0/schema_update.sql | 13 ++++----- .../controller/MobileAppBundleController.java | 4 +-- .../server/controller/MobileV2Controller.java | 18 ++++++------- .../controller/QrCodeSettingsController.java | 8 +++--- .../controller/SystemInfoController.java | 2 +- .../MobileAppBundleControllerTest.java | 18 ++++++------- .../dao/mobile/MobileAppBundleService.java | 4 +-- .../mobile/MobileAppBundleServiceImpl.java | 10 +++---- .../dao/mobile/QrCodeSettingService.java | 4 +-- .../dao/mobile/QrCodeSettingServiceImpl.java | 4 +-- .../dao/sql/mobile/JpaMobileAppDao.java | 13 ++++----- .../main/resources/sql/schema-entities.sql | 27 +++++++++---------- 12 files changed, 60 insertions(+), 65 deletions(-) diff --git a/application/src/main/data/upgrade/3.8.0/schema_update.sql b/application/src/main/data/upgrade/3.8.0/schema_update.sql index f9fd1e9181..fd72773b09 100644 --- a/application/src/main/data/upgrade/3.8.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.0/schema_update.sql @@ -21,13 +21,11 @@ CREATE TABLE IF NOT EXISTS mobile_app_bundle ( created_time bigint NOT NULL, tenant_id uuid, title varchar(255), - android_app_id uuid, - ios_app_id uuid, description varchar(1024), + android_app_id uuid UNIQUE, + ios_app_id uuid UNIQUE, layout_config varchar(16384), oauth2_enabled boolean, - CONSTRAINT android_app_id_unq_key UNIQUE (android_app_id), - CONSTRAINT ios_app_id_unq_key UNIQUE (ios_app_id), CONSTRAINT fk_android_app_id FOREIGN KEY (android_app_id) REFERENCES mobile_app(id), CONSTRAINT fk_ios_app_id FOREIGN KEY (ios_app_id) REFERENCES mobile_app(id) ); @@ -43,7 +41,7 @@ DO $$ BEGIN -- in case of running the upgrade script a second time - IF EXISTS(SELECT * FROM information_schema.tables WHERE table_name='mobile_app_oauth2_client') THEN + IF EXISTS(SELECT * FROM information_schema.tables WHERE table_name = 'mobile_app_oauth2_client') THEN ALTER TABLE mobile_app_oauth2_client RENAME TO mobile_app_bundle_oauth2_client; ALTER TABLE mobile_app_bundle_oauth2_client DROP CONSTRAINT IF EXISTS fk_domain; ALTER TABLE mobile_app_bundle_oauth2_client RENAME COLUMN mobile_app_id TO mobile_app_bundle_id; @@ -63,7 +61,7 @@ $$ mobileAppRecord RECORD; BEGIN -- in case of running the upgrade script a second time - IF EXISTS(SELECT * FROM information_schema.columns WHERE table_name='mobile_app' and column_name='oauth2_enabled') THEN + IF EXISTS(SELECT * FROM information_schema.columns WHERE table_name = 'mobile_app' and column_name = 'oauth2_enabled') THEN UPDATE mobile_app SET platform_type = 'ANDROID' WHERE platform_type IS NULL; UPDATE mobile_app SET status = 'PUBLISHED' WHERE mobile_app.status IS NULL; FOR mobileAppRecord IN SELECT * FROM mobile_app @@ -104,6 +102,7 @@ $$ IF EXISTS(SELECT * FROM information_schema.columns WHERE table_name = 'qr_code_settings' and column_name = 'android_config') THEN FOR qrCodeRecord IN SELECT * FROM qr_code_settings LOOP + generatedBundleId := NULL; -- migrate android config SELECT id into androidAppId FROM mobile_app WHERE pkg_name = qrCodeRecord.android_config::jsonb ->> 'appPackage' AND platform_type = 'ANDROID'; IF androidAppId IS NULL THEN @@ -117,6 +116,7 @@ $$ UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId WHERE id = qrCodeRecord.id; ELSE UPDATE mobile_app SET qr_code_config = qrCodeRecord.android_config::jsonb || '{"type": "ANDROID"}'::jsonb WHERE id = androidAppId; + UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.android_app_id = androidAppId) WHERE id = qrCodeRecord.id; END IF; -- migrate ios config @@ -137,6 +137,7 @@ $$ END IF; ELSE UPDATE mobile_app SET qr_code_config = qrCodeRecord.ios_config::jsonb || '{"type": "IOS"}'::jsonb WHERE id = iosAppId; + UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.ios_app_id = iosAppId) WHERE id = qrCodeRecord.id; END IF; END LOOP; END IF; diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java index 0853cb9eb2..96ddc95dcf 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java @@ -64,10 +64,10 @@ 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." + + notes = "Create or update the Mobile app bundle that represents tha pair of ANDROID and IOS app and " + + "mobile settings like oauth2 clients, self-registration and layout configuration." + "When creating mobile app bundle, platform generates Mobile App Bundle Id as " + UUID_WIKI_LINK + "The newly created Mobile App Bundle Id will be present in the response. " + - "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") diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java index b06d280b9a..abe3526bca 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java @@ -40,21 +40,19 @@ import java.util.List; public class MobileV2Controller extends BaseController { @GetMapping(value = "/api/noauth/mobile") - public LoginMobileInfo getMobileUserLoginSettings(@Parameter(description = "Mobile application package name") - @RequestParam String pkgName, - @Parameter(description = "Platform type", - schema = @Schema(allowableValues = {"ANDROID", "IOS"})) - @RequestParam PlatformType platform) { + public LoginMobileInfo getLoginMobileInfo(@Parameter(description = "Mobile application package name") + @RequestParam String pkgName, + @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) + @RequestParam PlatformType platform) { List oauth2Clients = oAuth2ClientService.findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(pkgName, platform); return new LoginMobileInfo(oauth2Clients); } @GetMapping(value = "/api/auth/mobile") - public UserMobileInfo getMobileUserSettings(@Parameter(description = "Mobile application package name") - @RequestParam String pkgName, - @Parameter(description = "Platform type", - schema = @Schema(allowableValues = {"ANDROID", "IOS"})) - @RequestParam PlatformType platform) throws ThingsboardException { + public UserMobileInfo getUserMobileInfo(@Parameter(description = "Mobile application package name") + @RequestParam String pkgName, + @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) + @RequestParam PlatformType platform) throws ThingsboardException { SecurityUser securityUser = getCurrentUser(); User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId()); HomeDashboardInfo homeDashboardInfo = getHomeDashboardInfo(securityUser, user.getAdditionalInfo()); diff --git a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java index c1ce037a11..12b47ba581 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java @@ -113,7 +113,7 @@ public class QrCodeSettingsController extends BaseController { @GetMapping(value = "/.well-known/apple-app-site-association") public ResponseEntity getAppleAppSiteAssociation() { IosQrCodeConfig iosQrCodeConfig = (IosQrCodeConfig) qrCodeSettingService.findAppQrCodeConfig(TenantId.SYS_TENANT_ID, IOS); - if (iosQrCodeConfig != null && iosQrCodeConfig.isEnabled() && iosQrCodeConfig.getAppId() != null) { + if (iosQrCodeConfig != null && iosQrCodeConfig.isEnabled()) { return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, iosQrCodeConfig.getAppId()))); } else { return ResponseEntity.notFound().build(); @@ -139,7 +139,7 @@ public class QrCodeSettingsController extends BaseController { public QrCodeSettings getMobileAppSettings() throws ThingsboardException { SecurityUser currentUser = getCurrentUser(); accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ); - return qrCodeSettingService.getQrCodeSettings(TenantId.SYS_TENANT_ID); + return qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID); } @ApiOperation(value = "Get the deep link to the associated mobile application (getMobileAppDeepLink)", @@ -150,7 +150,7 @@ public class QrCodeSettingsController extends BaseController { String secret = mobileAppSecretService.generateMobileAppSecret(getCurrentUser()); String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, null, request); String platformDomain = new URI(baseUrl).getHost(); - QrCodeSettings qrCodeSettings = qrCodeSettingService.getQrCodeSettings(TenantId.SYS_TENANT_ID); + QrCodeSettings qrCodeSettings = qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID); String appDomain; if (!qrCodeSettings.isUseDefaultApp()) { appDomain = platformDomain; @@ -175,7 +175,7 @@ public class QrCodeSettingsController extends BaseController { @GetMapping(value = "/api/noauth/qr") public ResponseEntity getApplicationRedirect(@RequestHeader(value = "User-Agent") String userAgent) { - QrCodeSettings qrCodeSettings = qrCodeSettingService.getQrCodeSettings(TenantId.SYS_TENANT_ID); + QrCodeSettings qrCodeSettings = qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID); boolean useDefaultApp = qrCodeSettings.isUseDefaultApp(); if (userAgent.contains("Android")) { String googlePlayLink = useDefaultApp ? qrCodeSettings.getDefaultGooglePlayLink() : getStoreLink(qrCodeSettings.getMobileAppBundleId(), ANDROID); diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 18d632345b..3b90def778 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -142,7 +142,7 @@ public class SystemInfoController extends BaseController { DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration(); systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize()); } - systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.getQrCodeSettings(TenantId.SYS_TENANT_ID)) + systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) .map(QrCodeSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage) .orElse(false)); return systemParams; diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java index b81ee82ecd..1cc0862fa8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java @@ -58,10 +58,10 @@ public class MobileAppBundleControllerTest extends AbstractControllerTest { public void setUp() throws Exception { loginSysAdmin(); - androidApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.android.package", PlatformType.ANDROID, true); + androidApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.android.package", PlatformType.ANDROID); androidApp = doPost("/api/mobile/app", androidApp, MobileApp.class); - iosApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.ios.package", PlatformType.IOS, true); + iosApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.ios.package", PlatformType.IOS); iosApp = doPost("/api/mobile/app", iosApp, MobileApp.class); } @@ -143,13 +143,13 @@ public class MobileAppBundleControllerTest extends AbstractControllerTest { 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; + private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, PlatformType platformType) { + MobileApp mobileApp = new MobileApp(); + mobileApp.setTenantId(tenantId); + mobileApp.setPkgName(mobileAppName); + mobileApp.setPlatformType(platformType); + mobileApp.setAppSecret(StringUtils.randomAlphanumeric(24)); + return mobileApp; } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java index ba1c7ae3ef..af296f89f2 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java @@ -31,8 +31,6 @@ public interface MobileAppBundleService extends EntityDaoService { MobileAppBundle saveMobileAppBundle(TenantId tenantId, MobileAppBundle mobileAppBundle); - void deleteMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId); - MobileAppBundle findMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId); PageData findMobileAppBundleInfosByTenantId(TenantId tenantId, PageLink pageLink); @@ -43,5 +41,7 @@ public interface MobileAppBundleService extends EntityDaoService { MobileAppBundle findMobileAppBundleByPkgNameAndPlatform(TenantId tenantId, String pkgName, PlatformType platform); + void deleteMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId); + void deleteMobileAppBundlesByTenantId(TenantId tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java index 388e9cb623..865b693d48 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java @@ -54,10 +54,10 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements private MobileAppBundleDao mobileAppBundleDao; @Override - public MobileAppBundle saveMobileAppBundle(TenantId tenantId, MobileAppBundle mobileApp) { - log.trace("Executing saveMobileApp [{}]", mobileApp); + public MobileAppBundle saveMobileAppBundle(TenantId tenantId, MobileAppBundle mobileAppBundle) { + log.trace("Executing saveMobileAppBundle [{}]", mobileAppBundle); try { - MobileAppBundle savedMobileApp = mobileAppBundleDao.save(tenantId, mobileApp); + MobileAppBundle savedMobileApp = mobileAppBundleDao.save(tenantId, mobileAppBundle); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(savedMobileApp).build()); return savedMobileApp; } catch (Exception e) { @@ -100,7 +100,7 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements @Override public MobileAppBundleInfo findMobileAppBundleInfoById(TenantId tenantId, MobileAppBundleId mobileAppIdBundle) { - log.trace("Executing findMobileAppInfoById [{}] [{}]", tenantId, mobileAppIdBundle); + log.trace("Executing findMobileAppBundleInfoById [{}] [{}]", tenantId, mobileAppIdBundle); MobileAppBundleInfo mobileAppBundleInfo = mobileAppBundleDao.findInfoById(tenantId, mobileAppIdBundle); if (mobileAppBundleInfo != null) { fetchOauth2Clients(mobileAppBundleInfo); @@ -133,7 +133,7 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements @Override public MobileAppBundle findMobileAppBundleByPkgNameAndPlatform(TenantId tenantId, String pkgName, PlatformType platform) { - log.trace("Executing findMobileAppBundle, tenantId [{}], pkgName [{}], platform [{}]", tenantId, pkgName, platform); + log.trace("Executing findMobileAppBundleByPkgNameAndPlatform, tenantId [{}], pkgName [{}], platform [{}]", tenantId, pkgName, platform); return mobileAppBundleDao.findByPkgNameAndPlatform(tenantId, pkgName, platform); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java index 48d221af9d..9c5a19c175 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java @@ -22,9 +22,9 @@ import org.thingsboard.server.common.data.oauth2.PlatformType; public interface QrCodeSettingService { - QrCodeSettings saveQrCodeSettings(TenantId tenantId, QrCodeSettings settings); + QrCodeSettings saveQrCodeSettings(TenantId tenantId, QrCodeSettings qrCodeSettings); - QrCodeSettings getQrCodeSettings(TenantId tenantId); + QrCodeSettings findQrCodeSettings(TenantId tenantId); QrCodeConfig findAppQrCodeConfig(TenantId sysTenantId, PlatformType platformType); diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java index 5e6ae2fcef..0dc672585c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java @@ -67,7 +67,7 @@ public class QrCodeSettingServiceImpl extends AbstractCachedEntityService qrCodeSettingsDao.findByTenantId(tenantId), true); @@ -77,7 +77,7 @@ public class QrCodeSettingServiceImpl extends AbstractCachedEntityService @Override 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; - } + return switch (platformType) { + case ANDROID -> DaoUtil.getData(mobileAppRepository.findAndroidAppByBundleId(mobileAppBundleId.getId())); + case IOS -> DaoUtil.getData(mobileAppRepository.findIOSAppByBundleId(mobileAppBundleId.getId())); + default -> null; + }; } @Override diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 762e4b6670..22a9fcd7c7 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -637,23 +637,22 @@ CREATE TABLE IF NOT EXISTS mobile_app ( platform_type varchar(32), status varchar(32), version_info varchar(16384), - qr_code_config varchar(16384) + qr_code_config varchar(16384), + CONSTRAINT pkg_platform_unique UNIQUE (pkg_name, platform_type) ); CREATE TABLE IF NOT EXISTS mobile_app_bundle ( - id uuid NOT NULL CONSTRAINT mobile_app_bundle_pkey PRIMARY KEY, - created_time bigint NOT NULL, - tenant_id uuid, - title varchar(255), - android_app_id uuid, - ios_app_id uuid, - description varchar(1024), - layout_config varchar(16384), - oauth2_enabled boolean, - CONSTRAINT android_app_id_unq_key UNIQUE (android_app_id), - CONSTRAINT ios_app_id_unq_key UNIQUE (ios_app_id), - CONSTRAINT fk_android_app_id FOREIGN KEY (android_app_id) REFERENCES mobile_app(id), - CONSTRAINT fk_ios_app_id FOREIGN KEY (ios_app_id) REFERENCES mobile_app(id) + id uuid NOT NULL CONSTRAINT mobile_app_bundle_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid, + title varchar(255), + description varchar(1024), + android_app_id uuid UNIQUE, + ios_app_id uuid UNIQUE, + layout_config varchar(16384), + oauth2_enabled boolean, + CONSTRAINT fk_android_app_id FOREIGN KEY (android_app_id) REFERENCES mobile_app(id), + CONSTRAINT fk_ios_app_id FOREIGN KEY (ios_app_id) REFERENCES mobile_app(id) ); CREATE TABLE IF NOT EXISTS domain_oauth2_client ( From b512d9aa4abb228f85f6ce50538bb463ebf90442 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 8 Oct 2024 18:46:15 +0300 Subject: [PATCH 04/48] fixed MobileV2Controller with authorization checks --- .../server/controller/MobileV2Controller.java | 28 ++++++++++++++++--- .../server/dao/mobile/MobileAppService.java | 2 ++ .../common/data/mobile/LoginMobileInfo.java | 2 +- .../server/dao/mobile/MobileAppDao.java | 2 ++ .../dao/mobile/MobileAppServiceImpl.java | 9 ++++-- .../dao/sql/mobile/JpaMobileAppBundleDao.java | 2 +- .../dao/sql/mobile/JpaMobileAppDao.java | 5 ++++ .../sql/mobile/MobileAppBundleRepository.java | 3 +- .../dao/sql/mobile/MobileAppRepository.java | 3 ++ 9 files changed, 46 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java index abe3526bca..764c18bfd7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java @@ -18,46 +18,66 @@ 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.security.access.prepost.PreAuthorize; 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.LoginMobileInfo; +import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.MobileAppVersionInfo; import org.thingsboard.server.common.data.mobile.UserMobileInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; import java.util.List; +import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; + @RequiredArgsConstructor @RestController @TbCoreComponent public class MobileV2Controller extends BaseController { + @ApiOperation(value = "Get mobile app login info (getLoginMobileInfo)") @GetMapping(value = "/api/noauth/mobile") public LoginMobileInfo getLoginMobileInfo(@Parameter(description = "Mobile application package name") @RequestParam String pkgName, @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) @RequestParam PlatformType platform) { List oauth2Clients = oAuth2ClientService.findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(pkgName, platform); - return new LoginMobileInfo(oauth2Clients); + MobileApp mobileApp = mobileAppService.findMobileAppByPkgNameAndPlatformType(pkgName, platform); + return new LoginMobileInfo(oauth2Clients, mobileApp != null ? mobileApp.getVersionInfo() : null); } - @GetMapping(value = "/api/auth/mobile") + @ApiOperation(value = "Get user mobile app basic info (getUserMobileInfo)", notes = AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/api/mobile") public UserMobileInfo getUserMobileInfo(@Parameter(description = "Mobile application package name") @RequestParam String pkgName, @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) @RequestParam PlatformType platform) throws ThingsboardException { SecurityUser securityUser = getCurrentUser(); User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId()); - HomeDashboardInfo homeDashboardInfo = getHomeDashboardInfo(securityUser, user.getAdditionalInfo()); + HomeDashboardInfo homeDashboardInfo = securityUser.isSystemAdmin() ? null : getHomeDashboardInfo(securityUser, user.getAdditionalInfo()); MobileAppBundle mobileAppBundle = mobileAppBundleService.findMobileAppBundleByPkgNameAndPlatform(securityUser.getTenantId(), pkgName, platform); return new UserMobileInfo(user, homeDashboardInfo, mobileAppBundle != null ? mobileAppBundle.getLayoutConfig() : null); } + @ApiOperation(value = "Get mobile app version info (getMobileVersionInfo)") + @GetMapping(value = "/api/mobile/versionInfo") + public MobileAppVersionInfo getMobileVersionInfo(@Parameter(description = "Mobile application package name") + @RequestParam String pkgName, + @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) + @RequestParam PlatformType platform) { + MobileApp mobileApp = mobileAppService.findMobileAppByPkgNameAndPlatformType(pkgName, platform); + return mobileApp != null ? mobileApp.getVersionInfo() : null; + } + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java index 01ebe81d30..63ca222cde 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java @@ -34,6 +34,8 @@ public interface MobileAppService extends EntityDaoService { MobileApp findByBundleIdAndPlatformType(TenantId tenantId, MobileAppBundleId mobileAppBundleId, PlatformType platformType); + MobileApp findMobileAppByPkgNameAndPlatformType(String pkgName, PlatformType platform); + void deleteMobileAppById(TenantId tenantId, MobileAppId mobileAppId); void deleteMobileAppsByTenantId(TenantId tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/LoginMobileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/LoginMobileInfo.java index f146291123..0556e2169d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/LoginMobileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/LoginMobileInfo.java @@ -19,5 +19,5 @@ import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; import java.util.List; -public record LoginMobileInfo(List oAuth2ClientLoginInfos) { +public record LoginMobileInfo(List oAuth2ClientLoginInfos, MobileAppVersionInfo versionInfo) { } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java index 091e61327c..683cb88efd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java @@ -30,4 +30,6 @@ public interface MobileAppDao extends Dao { PageData findByTenantId(TenantId tenantId, PageLink pageLink); void deleteByTenantId(TenantId tenantId); + + MobileApp findByPkgNameAndPlatformType(TenantId tenantId, String pkgName, PlatformType platform); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java index a471b30589..6ff1130221 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java @@ -19,15 +19,12 @@ 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.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.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; @@ -102,6 +99,12 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil return mobileAppDao.findByBundleIdAndPlatformType(tenantId, mobileAppBundleId, platformType); } + @Override + public MobileApp findMobileAppByPkgNameAndPlatformType(String pkgName, PlatformType platform) { + log.trace("Executing findMobileAppByPkgNameAndPlatformType, pkgName [{}], platform [{}]", pkgName, platform); + return mobileAppDao.findByPkgNameAndPlatformType(TenantId.SYS_TENANT_ID, pkgName, platform); + } + @Override public void deleteByTenantId(TenantId tenantId) { deleteMobileAppsByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java index 0d9bc0dbf5..b2b9d913f3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java @@ -84,7 +84,7 @@ public class JpaMobileAppBundleDao extends JpaAbstractDao mobileAppRepository.deleteByTenantId(tenantId.getId()); } + @Override + public MobileApp findByPkgNameAndPlatformType(TenantId tenantId, String pkgName, PlatformType platform) { + return DaoUtil.getData(mobileAppRepository.findByPkgNameAndPlatformType(pkgName, platform)); + } + @Override public EntityType getEntityType() { return EntityType.MOBILE_APP; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java index 846062ec4d..7cc60ddd00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java @@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.dao.model.sql.MobileAppBundleEntity; import org.thingsboard.server.dao.model.sql.MobileAppBundleInfoEntity; @@ -51,7 +52,7 @@ public interface MobileAppBundleRepository extends JpaRepository Date: Wed, 9 Oct 2024 18:22:59 +0300 Subject: [PATCH 05/48] Rename QR code API --- .../controller/QrCodeSettingsController.java | 6 +-- .../QrCodeSettingsControllerTest.java | 40 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java index 12b47ba581..d554013931 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java @@ -123,7 +123,7 @@ public class QrCodeSettingsController 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/qr/settings") + @PostMapping(value = "/api/mobile/qr/settings") public QrCodeSettings saveMobileAppSettings(@Parameter(description = "A JSON value representing the mobile apps configuration") @RequestBody QrCodeSettings qrCodeSettings) throws ThingsboardException { SecurityUser currentUser = getCurrentUser(); @@ -135,7 +135,7 @@ public class QrCodeSettingsController extends BaseController { @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/qr/settings") + @GetMapping(value = "/api/mobile/qr/settings") public QrCodeSettings getMobileAppSettings() throws ThingsboardException { SecurityUser currentUser = getCurrentUser(); accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ); @@ -145,7 +145,7 @@ public class QrCodeSettingsController extends BaseController { @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/qr/deepLink", produces = "text/plain") + @GetMapping(value = "/api/mobile/qr/deepLink", produces = "text/plain") public String getMobileAppDeepLink(HttpServletRequest request) throws ThingsboardException, URISyntaxException { String secret = mobileAppSecretService.generateMobileAppSecret(getCurrentUser()); String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, null, request); diff --git a/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java index 88949a7e11..81512d7fa5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java @@ -94,7 +94,7 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { mobileAppBundle = doPost("/api/mobile/bundle", mobileAppBundle, MobileAppBundle.class); - QrCodeSettings qrCodeSettings = doGet("/api/qr/settings", QrCodeSettings.class); + QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class); QRCodeConfig qrCodeConfig = new QRCodeConfig(); qrCodeConfig.setQrCodeLabel(TEST_LABEL); qrCodeSettings.setUseDefaultApp(true); @@ -123,61 +123,61 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { @Test public void testSaveQrCodeSettings() throws Exception { loginSysAdmin(); - QrCodeSettings qrCodeSettings = doGet("/api/qr/settings", QrCodeSettings.class); + QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class); assertThat(qrCodeSettings.getQrCodeConfig().getQrCodeLabel()).isEqualTo(TEST_LABEL); assertThat(qrCodeSettings.isUseDefaultApp()).isTrue(); qrCodeSettings.setUseDefaultApp(false); qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId()); - doPost("/api/qr/settings", qrCodeSettings) + doPost("/api/mobile/qr/settings", qrCodeSettings) .andExpect(status().isOk()); - QrCodeSettings updatedQrCodeSettings = doGet("/api/qr/settings", QrCodeSettings.class); + QrCodeSettings updatedQrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class); assertThat(updatedQrCodeSettings.isUseDefaultApp()).isFalse(); } @Test public void testShouldNotSaveQrCodeSettingsWithoutRequiredConfig() throws Exception { loginSysAdmin(); - QrCodeSettings qrCodeSettings = doGet("/api/qr/settings", QrCodeSettings.class); + QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class); qrCodeSettings.setUseDefaultApp(false); qrCodeSettings.setQrCodeConfig(null); qrCodeSettings.setMobileAppBundleId(null); - doPost("/api/qr/settings", qrCodeSettings) + doPost("/api/mobile/qr/settings", qrCodeSettings) .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString("Mobile app bundle is required to use custom application!"))); qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId()); - doPost("/api/qr/settings", qrCodeSettings) + doPost("/api/mobile/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) + doPost("/api/mobile/qr/settings", qrCodeSettings) .andExpect(status().isOk()); } @Test public void testShouldSaveQrCodeSettingsForDefaultApp() throws Exception { loginSysAdmin(); - QrCodeSettings qrCodeSettings = doGet("/api/qr/settings", QrCodeSettings.class); + QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class); qrCodeSettings.setUseDefaultApp(true); qrCodeSettings.setMobileAppBundleId(null); - doPost("/api/qr/settings", qrCodeSettings) + doPost("/api/mobile/qr/settings", qrCodeSettings) .andExpect(status().isOk()); } @Test public void testGetApplicationAssociations() throws Exception { loginSysAdmin(); - QrCodeSettings qrCodeSettings = doGet("/api/qr/settings", QrCodeSettings.class); + QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class); qrCodeSettings.setUseDefaultApp(true); qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId()); - doPost("/api/qr/settings", qrCodeSettings) + doPost("/api/mobile/qr/settings", qrCodeSettings) .andExpect(status().isOk()); JsonNode assetLinks = doGet("/.well-known/assetlinks.json", JsonNode.class); @@ -191,7 +191,7 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { @Test public void testGetMobileDeepLink() throws Exception { loginSysAdmin(); - String deepLink = doGet("/api/qr/deepLink", String.class); + String deepLink = doGet("/api/mobile/qr/deepLink", String.class); Pattern expectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)&host=([^&]+)\""); Matcher parsedDeepLink = expectedPattern.matcher(deepLink); @@ -206,7 +206,7 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { assertThat(jwtPair).isNotNull(); loginTenantAdmin(); - String tenantDeepLink = doGet("/api/qr/deepLink", String.class); + String tenantDeepLink = doGet("/api/mobile/qr/deepLink", String.class); Matcher tenantParsedDeepLink = expectedPattern.matcher(tenantDeepLink); assertThat(tenantParsedDeepLink.matches()).isTrue(); String tenantSecret = tenantParsedDeepLink.group(2); @@ -215,7 +215,7 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { assertThat(tenantJwtPair).isNotNull(); loginCustomerUser(); - String customerDeepLink = doGet("/api/qr/deepLink", String.class); + String customerDeepLink = doGet("/api/mobile/qr/deepLink", String.class); Matcher customerParsedDeepLink = expectedPattern.matcher(customerDeepLink); assertThat(customerParsedDeepLink.matches()).isTrue(); String customerSecret = customerParsedDeepLink.group(2); @@ -225,25 +225,25 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { // update mobile setting to use custom one loginSysAdmin(); - QrCodeSettings qrCodeSettings = doGet("/api/qr/settings", QrCodeSettings.class); + QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class); qrCodeSettings.setUseDefaultApp(false); qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId()); - doPost("/api/qr/settings", qrCodeSettings); + doPost("/api/mobile/qr/settings", qrCodeSettings); - String customAppDeepLink = doGet("/api/qr/deepLink", String.class); + String customAppDeepLink = doGet("/api/mobile/qr/deepLink", String.class); Pattern customAppExpectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)\""); Matcher customAppParsedDeepLink = customAppExpectedPattern.matcher(customAppDeepLink); assertThat(customAppParsedDeepLink.matches()).isTrue(); assertThat(customAppParsedDeepLink.group(1)).isEqualTo("localhost"); loginTenantAdmin(); - String tenantCustomAppDeepLink = doGet("/api/qr/deepLink", String.class); + String tenantCustomAppDeepLink = doGet("/api/mobile/qr/deepLink", String.class); Matcher tenantCustomAppParsedDeepLink = customAppExpectedPattern.matcher(tenantCustomAppDeepLink); assertThat(tenantCustomAppParsedDeepLink.matches()).isTrue(); assertThat(tenantCustomAppParsedDeepLink.group(1)).isEqualTo("localhost"); loginCustomerUser(); - String customerCustomAppDeepLink = doGet("/api/qr/deepLink", String.class); + String customerCustomAppDeepLink = doGet("/api/mobile/qr/deepLink", String.class); Matcher customerCustomAppParsedDeepLink = customAppExpectedPattern.matcher(customerCustomAppDeepLink); assertThat(customerCustomAppParsedDeepLink.matches()).isTrue(); assertThat(customerCustomAppParsedDeepLink.group(1)).isEqualTo("localhost"); From 06ad4bdc43c60d0c44131509936492f1f50221a9 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 10 Oct 2024 12:32:17 +0300 Subject: [PATCH 06/48] added platformType request param to /mobile/app API endpoint --- .../controller/MobileAppController.java | 7 ++- .../DefaultSystemDataLoaderService.java | 2 +- .../controller/MobileAppControllerTest.java | 37 ++++++++---- .../QrCodeSettingsControllerTest.java | 23 ++++--- .../server/dao/mobile/MobileAppService.java | 2 +- .../server/common/data/mobile/MobileApp.java | 4 +- .../mobile/MobileAppBundleServiceImpl.java | 5 ++ .../server/dao/mobile/MobileAppDao.java | 2 +- .../dao/mobile/MobileAppServiceImpl.java | 4 +- .../MobileAppBundleDataValidator.java | 60 +++++++++++++++++++ .../dao/sql/mobile/JpaMobileAppDao.java | 4 +- .../dao/sql/mobile/MobileAppRepository.java | 2 + .../dao/service/MobileAppServiceTest.java | 12 ++-- 13 files changed, 125 insertions(+), 39 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppBundleDataValidator.java diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java index 4fe2d4cd2a..1e3d8e31f9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java @@ -31,6 +31,7 @@ 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.mobile.MobileApp; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.config.annotations.ApiOperation; @@ -76,7 +77,9 @@ public class MobileAppController extends BaseController { @ApiOperation(value = "Get mobile app infos (getTenantMobileAppInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @GetMapping(value = "/mobile/app") - public PageData getTenantMobileApps(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + public PageData getTenantMobileApps(@Parameter(description = "Platform type: ANDROID or IOS") + @RequestParam(required = false) PlatformType platformType, + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, @@ -88,7 +91,7 @@ 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.findMobileAppsByTenantId(getTenantId(), pageLink); + return mobileAppService.findMobileAppsByTenantId(getTenantId(), platformType, pageLink); } @ApiOperation(value = "Get mobile info by id (getMobileAppInfoById)", notes = SYSTEM_AUTHORITY_PARAGRAPH) diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 52ef9b203f..1e92b787bb 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -308,7 +308,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { jwtSettingsService.saveJwtSettings(jwtSettings); } - List mobiles = mobileAppDao.findByTenantId(TenantId.SYS_TENANT_ID, new PageLink(Integer.MAX_VALUE,0)).getData(); + List mobiles = mobileAppDao.findByTenantId(TenantId.SYS_TENANT_ID, null, new PageLink(Integer.MAX_VALUE,0)).getData(); if (CollectionUtils.isNotEmpty(mobiles)) { mobiles.stream() .filter(mobileApp -> !validateKeyLength(mobileApp.getAppSecret())) diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java index e4f9ab583a..3493538266 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java @@ -21,10 +21,10 @@ 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.AndroidQrCodeConfig; import org.thingsboard.server.common.data.mobile.IosQrCodeConfig; import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -59,7 +59,7 @@ public class MobileAppControllerTest extends AbstractControllerTest { PageData 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"); + MobileApp mobileApp = validMobileApp("my.test.package", PlatformType.ANDROID); MobileApp savedMobileApp = doPost("/api/mobile/app", mobileApp, MobileApp.class); PageData pageData2 = doGetTypedWithPageLink("/api/mobile/app?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0)); @@ -76,7 +76,7 @@ public class MobileAppControllerTest extends AbstractControllerTest { @Test public void testSaveMobileAppWithShortAppSecret() throws Exception { - MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "mobileApp.ce"); + MobileApp mobileApp = validMobileApp( "mobileApp.ce", PlatformType.ANDROID); mobileApp.setAppSecret("short"); doPost("/api/mobile/app", mobileApp) .andExpect(status().isBadRequest()) @@ -85,7 +85,7 @@ public class MobileAppControllerTest extends AbstractControllerTest { @Test public void testShouldNotSaveMobileAppWithWrongQrCodeConf() throws Exception { - MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "mobileApp.ce"); + MobileApp mobileApp = validMobileApp("mobileApp.ce", PlatformType.ANDROID); AndroidQrCodeConfig androidQrCodeConfig = AndroidQrCodeConfig.builder() .enabled(true) .appPackage(null) @@ -110,7 +110,7 @@ public class MobileAppControllerTest extends AbstractControllerTest { @Test public void testShouldNotSaveMobileAppWithWrongIosConf() throws Exception { - MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "mobileApp.ce"); + MobileApp mobileApp = validMobileApp("mobileApp.ce", PlatformType.ANDROID); IosQrCodeConfig iosQrCodeConfig = IosQrCodeConfig.builder() .enabled(true) .appId(null) @@ -127,12 +127,27 @@ public class MobileAppControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } - private MobileApp validMobileApp(TenantId tenantId, String mobileAppName) { - MobileApp MobileApp = new MobileApp(); - MobileApp.setTenantId(tenantId); - MobileApp.setPkgName(mobileAppName); - MobileApp.setAppSecret(StringUtils.randomAlphanumeric(24)); - return MobileApp; + @Test + public void testGetTenantAppsByPlatformTypeSaveMobileApp() throws Exception { + MobileApp androidApp = doPost("/api/mobile/app", validMobileApp("android.1", PlatformType.ANDROID), MobileApp.class); + MobileApp androidApp2 = doPost("/api/mobile/app", validMobileApp("android.2", PlatformType.ANDROID), MobileApp.class); + MobileApp iosApp = doPost("/api/mobile/app", validMobileApp("ios.1", PlatformType.IOS), MobileApp.class); + + PageData pageData = doGetTypedWithPageLink("/api/mobile/app?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0)); + assertThat(pageData.getData()).hasSize(3); + assertThat(pageData.getData()).containsExactlyInAnyOrder(androidApp, androidApp2, iosApp); + + PageData androidPageData = doGetTypedWithPageLink("/api/mobile/app?platformType=ANDROID&", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0)); + assertThat(androidPageData.getData()).hasSize(2); + assertThat(androidPageData.getData()).containsExactlyInAnyOrder(androidApp, androidApp2); + } + + private MobileApp validMobileApp(String mobileAppName, PlatformType platformType) { + MobileApp mobileApp = new MobileApp(); + mobileApp.setPkgName(mobileAppName); + mobileApp.setAppSecret(StringUtils.randomAlphanumeric(24)); + mobileApp.setPlatformType(platformType); + return mobileApp; } } diff --git a/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java index 81512d7fa5..b359c8b500 100644 --- a/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java @@ -23,14 +23,13 @@ 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.mobile.QrCodeSettings; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -68,7 +67,7 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { public void setUp() throws Exception { loginSysAdmin(); - MobileApp androidApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.android.package", PlatformType.ANDROID, true); + MobileApp androidApp = validMobileApp( "my.android.package", PlatformType.ANDROID); AndroidQrCodeConfig androidQrCodeConfig = AndroidQrCodeConfig.builder() .appPackage(ANDROID_PACKAGE_NAME) .sha256CertFingerprints(ANDROID_APP_SHA256) @@ -78,7 +77,7 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { 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); + MobileApp iosApp = validMobileApp( "my.ios.package", PlatformType.IOS); IosQrCodeConfig iosQrCodeConfig = IosQrCodeConfig.builder() .appId(APPLE_APP_ID) .enabled(true) @@ -101,7 +100,7 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { qrCodeSettings.setMobileAppBundleId(null); qrCodeSettings.setQrCodeConfig(qrCodeConfig); - doPost("/api/qr/settings", qrCodeSettings) + doPost("/api/mobile/qr/settings", qrCodeSettings) .andExpect(status().isOk()); } @@ -249,12 +248,12 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { 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; + private MobileApp validMobileApp(String mobileAppName, PlatformType platformType) { + MobileApp mobileApp = new MobileApp(); + mobileApp.setTenantId(tenantId); + mobileApp.setPkgName(mobileAppName); + mobileApp.setPlatformType(platformType); + mobileApp.setAppSecret(StringUtils.randomAlphanumeric(24)); + return mobileApp; } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java index 63ca222cde..12559080be 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java @@ -30,7 +30,7 @@ public interface MobileAppService extends EntityDaoService { MobileApp findMobileAppById(TenantId tenantId, MobileAppId mobileAppId); - PageData findMobileAppsByTenantId(TenantId tenantId, PageLink pageLink); + PageData findMobileAppsByTenantId(TenantId tenantId, PlatformType platformType, PageLink pageLink); MobileApp findByBundleIdAndPlatformType(TenantId tenantId, MobileAppBundleId mobileAppBundleId, PlatformType platformType); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java index 5e28815908..b4475a0c50 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java @@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -47,8 +48,9 @@ public class MobileApp extends BaseData implements HasTenantId, Has @Length(fieldName = "appSecret", min = 16, max = 2048, message = "must be at least 16 and max 2048 characters") private String appSecret; @Schema(description = "Application platform type: ANDROID or IOS", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull private PlatformType platformType; - @Schema(description = "Application status: PUBLISHED, DEPRECATED, SUSPENDED", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "Application status: PUBLISHED, DEPRECATED, SUSPENDED, DRAFT", requiredMode = Schema.RequiredMode.REQUIRED) private MobileAppStatus status; @Schema(description = "Application version info") @Valid diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java index 865b693d48..d0735631fe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java @@ -36,6 +36,7 @@ import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.oauth2.OAuth2ClientDao; +import org.thingsboard.server.dao.service.DataValidator; import java.util.Comparator; import java.util.List; @@ -52,10 +53,14 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements private OAuth2ClientDao oauth2ClientDao; @Autowired private MobileAppBundleDao mobileAppBundleDao; + @Autowired + private DataValidator mobileAppBundleDataValidator; + @Override public MobileAppBundle saveMobileAppBundle(TenantId tenantId, MobileAppBundle mobileAppBundle) { log.trace("Executing saveMobileAppBundle [{}]", mobileAppBundle); + mobileAppBundleDataValidator.validate(mobileAppBundle, b -> tenantId); try { MobileAppBundle savedMobileApp = mobileAppBundleDao.save(tenantId, mobileAppBundle); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(savedMobileApp).build()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java index 683cb88efd..26fd6a3118 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java @@ -27,7 +27,7 @@ public interface MobileAppDao extends Dao { MobileApp findByBundleIdAndPlatformType(TenantId tenantId, MobileAppBundleId mobileAppBundleId, PlatformType platformType); - PageData findByTenantId(TenantId tenantId, PageLink pageLink); + PageData findByTenantId(TenantId tenantId, PlatformType platformType, PageLink pageLink); void deleteByTenantId(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java index 6ff1130221..8e043c92eb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java @@ -71,9 +71,9 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil } @Override - public PageData findMobileAppsByTenantId(TenantId tenantId, PageLink pageLink) { + public PageData findMobileAppsByTenantId(TenantId tenantId, PlatformType platformType, PageLink pageLink) { log.trace("Executing findMobileAppInfosByTenantId [{}]", tenantId); - return mobileAppDao.findByTenantId(tenantId, pageLink); + return mobileAppDao.findByTenantId(tenantId, platformType, pageLink); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppBundleDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppBundleDataValidator.java new file mode 100644 index 0000000000..c7ac1f6a64 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppBundleDataValidator.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import lombok.AllArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.MobileAppBundle; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.mobile.MobileAppDao; +import org.thingsboard.server.dao.service.DataValidator; + +@Component +@AllArgsConstructor +public class MobileAppBundleDataValidator extends DataValidator { + + @Autowired + private MobileAppDao mobileAppDao; + + @Override + protected void validateDataImpl(TenantId tenantId, MobileAppBundle mobileAppBundle) { + MobileAppId androidAppId = mobileAppBundle.getAndroidAppId(); + if (androidAppId != null) { + MobileApp androidApp = mobileAppDao.findById(tenantId, androidAppId.getId()); + if (androidApp == null) { + throw new DataValidationException("Mobile app bundle refers to non-existing android app!"); + } + if (androidApp.getPlatformType() != PlatformType.ANDROID) { + throw new DataValidationException("Mobile app bundle refers to wrong android app! Platform type of specified app is " + androidApp.getPlatformType()); + } + } + MobileAppId iosAppId = mobileAppBundle.getIosAppId(); + if (iosAppId != null) { + MobileApp iosApp = mobileAppDao.findById(tenantId, iosAppId.getId()); + if (iosApp == null) { + throw new DataValidationException("Mobile app bundle refers to non-existing ios app!"); + } + if (iosApp.getPlatformType() != PlatformType.IOS) { + throw new DataValidationException("Mobile app bundle refers to wrong ios app! Platform type of specified app is " + iosApp.getPlatformType()); + } + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java index daa7bc56ad..5e1670bc4f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java @@ -60,8 +60,8 @@ public class JpaMobileAppDao extends JpaAbstractDao } @Override - public PageData findByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(mobileAppRepository.findByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); + public PageData findByTenantId(TenantId tenantId, PlatformType platformType, PageLink pageLink) { + return DaoUtil.toPageData(mobileAppRepository.findByTenantId(tenantId.getId(), platformType, pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppRepository.java index c463596069..c93fad31d9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppRepository.java @@ -30,8 +30,10 @@ import java.util.UUID; public interface MobileAppRepository extends JpaRepository { @Query("SELECT a FROM MobileAppEntity a WHERE a.tenantId = :tenantId AND " + + "(:platformType is NULL OR a.platformType = :platformType) AND" + "(:searchText is NULL OR ilike(a.pkgName, concat('%', :searchText, '%')) = true)") Page findByTenantId(@Param("tenantId") UUID tenantId, + @Param("platformType") PlatformType platformType, @Param("searchText") String searchText, Pageable pageable); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java index 0cf6cb53ad..90a9f0b6b8 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java @@ -21,6 +21,7 @@ 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.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.mobile.MobileAppService; @@ -48,7 +49,7 @@ public class MobileAppServiceTest extends AbstractServiceTest { @Test public void testSaveMobileApp() { - MobileApp MobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "mobileApp.ce", true); + MobileApp MobileApp = validMobileApp("mobileApp.ce", PlatformType.IOS); MobileApp savedMobileApp = mobileAppService.saveMobileApp(SYSTEM_TENANT_ID, MobileApp); MobileApp retrievedMobileApp = mobileAppService.findMobileAppById(savedMobileApp.getTenantId(), savedMobileApp.getId()); @@ -70,20 +71,19 @@ public class MobileAppServiceTest extends AbstractServiceTest { public void testGetTenantMobileApps() { List mobileApps = new ArrayList<>(); for (int i = 0; i < 5; i++) { - MobileApp oAuth2Client = validMobileApp(TenantId.SYS_TENANT_ID, StringUtils.randomAlphabetic(5), true); + MobileApp oAuth2Client = validMobileApp(StringUtils.randomAlphabetic(5), PlatformType.ANDROID); MobileApp savedOauth2Client = mobileAppService.saveMobileApp(SYSTEM_TENANT_ID, oAuth2Client); mobileApps.add(savedOauth2Client); } - PageData retrieved = mobileAppService.findMobileAppsByTenantId(TenantId.SYS_TENANT_ID, new PageLink(10, 0)); + PageData retrieved = mobileAppService.findMobileAppsByTenantId(TenantId.SYS_TENANT_ID, null, new PageLink(10, 0)); assertThat(retrieved.getData()).containsOnlyOnceElementsOf(mobileApps); } - - private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, boolean oauth2Enabled) { + private MobileApp validMobileApp(String mobileAppName, PlatformType platformType) { MobileApp MobileApp = new MobileApp(); - MobileApp.setTenantId(tenantId); MobileApp.setPkgName(mobileAppName); MobileApp.setAppSecret(StringUtils.randomAlphanumeric(24)); + MobileApp.setPlatformType(platformType); return MobileApp; } } From 8c9bb7a0db8589539380cea2f934abe089d93974 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 10 Oct 2024 14:08:12 +0300 Subject: [PATCH 07/48] constraint rename, EntityType enum fix --- application/src/main/data/upgrade/3.8.0/schema_update.sql | 1 + .../org/thingsboard/server/controller/BaseController.java | 6 +++--- .../java/org/thingsboard/server/common/data/EntityType.java | 2 +- .../server/dao/mobile/QrCodeSettingServiceImpl.java | 2 +- dao/src/main/resources/sql/schema-entities.sql | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/application/src/main/data/upgrade/3.8.0/schema_update.sql b/application/src/main/data/upgrade/3.8.0/schema_update.sql index fd72773b09..f296937154 100644 --- a/application/src/main/data/upgrade/3.8.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.0/schema_update.sql @@ -140,6 +140,7 @@ $$ UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.ios_app_id = iosAppId) WHERE id = qrCodeRecord.id; END IF; END LOOP; + ALTER TABLE qr_code_settings RENAME CONSTRAINT mobile_app_settings_tenant_id_unq_key TO qr_code_settings_tenant_id_unq_key; END IF; ALTER TABLE qr_code_settings DROP COLUMN IF EXISTS android_config, DROP COLUMN IF EXISTS ios_config; END; diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index d2f0884ba6..5999be7428 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -201,10 +201,10 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @TbCoreComponent public abstract class BaseController { - public static final String DASHBOARD_ID = "dashboardId"; + protected static final String DASHBOARD_ID = "dashboardId"; - public static final String HOME_DASHBOARD_ID = "homeDashboardId"; - public static final String HOME_DASHBOARD_HIDE_TOOLBAR = "homeDashboardHideToolbar"; + protected static final String HOME_DASHBOARD_ID = "homeDashboardId"; + protected static final String HOME_DASHBOARD_HIDE_TOOLBAR = "homeDashboardHideToolbar"; protected final Logger log = org.slf4j.LoggerFactory.getLogger(getClass()); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 89b434047b..579fa863ba 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -61,7 +61,7 @@ public enum EntityType { OAUTH2_CLIENT(35), DOMAIN(36), MOBILE_APP(37), - MOBILE_APP_BUNDLE(37); + MOBILE_APP_BUNDLE(38); @Getter private final int protoNumber; // Corresponds to EntityTypeProto diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java index 0dc672585c..4cebb26451 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java @@ -60,7 +60,7 @@ public class QrCodeSettingServiceImpl extends AbstractCachedEntityService Date: Thu, 10 Oct 2024 17:39:59 +0300 Subject: [PATCH 08/48] simplified data structure for mobile all store info object --- .../main/data/upgrade/3.8.0/schema_update.sql | 8 +-- .../controller/QrCodeSettingsController.java | 19 +++---- .../controller/MobileAppControllerTest.java | 46 ----------------- .../QrCodeSettingsControllerTest.java | 14 +++--- .../data/mobile/AndroidQrCodeConfig.java | 49 ------------------- .../server/common/data/mobile/MobileApp.java | 6 +-- .../common/data/mobile/QrCodeConfig.java | 38 -------------- .../{IosQrCodeConfig.java => StoreInfo.java} | 18 ++----- .../dao/mobile/QrCodeSettingService.java | 4 +- .../dao/mobile/QrCodeSettingServiceImpl.java | 6 +-- .../server/dao/model/sql/MobileAppEntity.java | 6 +-- 11 files changed, 34 insertions(+), 180 deletions(-) delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidQrCodeConfig.java delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeConfig.java rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{IosQrCodeConfig.java => StoreInfo.java} (67%) diff --git a/application/src/main/data/upgrade/3.8.0/schema_update.sql b/application/src/main/data/upgrade/3.8.0/schema_update.sql index f296937154..198d43b5b8 100644 --- a/application/src/main/data/upgrade/3.8.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.0/schema_update.sql @@ -109,13 +109,13 @@ $$ androidAppId := uuid_generate_v4(); INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, qr_code_config) VALUES (androidAppId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, - qrCodeRecord.android_config::jsonb ->> 'appPackage', 'ANDROID', 'PUBLISHED', qrCodeRecord.android_config::jsonb || '{"type": "ANDROID"}'::jsonb); + qrCodeRecord.android_config::jsonb ->> 'appPackage', 'ANDROID', 'PUBLISHED', qrCodeRecord.android_config::jsonb - 'appPackage'); generatedBundleId := uuid_generate_v4(); INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id) VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, 'App bundle for qr code', androidAppId); UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId WHERE id = qrCodeRecord.id; ELSE - UPDATE mobile_app SET qr_code_config = qrCodeRecord.android_config::jsonb || '{"type": "ANDROID"}'::jsonb WHERE id = androidAppId; + UPDATE mobile_app SET qr_code_config = qrCodeRecord.android_config::jsonb - 'appPackage' WHERE id = androidAppId; UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.android_app_id = androidAppId) WHERE id = qrCodeRecord.id; END IF; @@ -126,7 +126,7 @@ $$ iosAppId := uuid_generate_v4(); INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, qr_code_config) VALUES (iosAppId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, - iosPkgName, 'IOS', 'PUBLISHED', qrCodeRecord.ios_config::jsonb || '{"type": "IOS"}'::jsonb); + iosPkgName, 'IOS', 'PUBLISHED', qrCodeRecord.ios_config); IF generatedBundleId IS NULL THEN generatedBundleId := uuid_generate_v4(); INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, ios_app_id) @@ -136,7 +136,7 @@ $$ UPDATE mobile_app_bundle SET ios_app_id = iosAppId WHERE id = generatedBundleId; END IF; ELSE - UPDATE mobile_app SET qr_code_config = qrCodeRecord.ios_config::jsonb || '{"type": "IOS"}'::jsonb WHERE id = iosAppId; + UPDATE mobile_app SET qr_code_config = qrCodeRecord.ios_config WHERE id = iosAppId; UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.ios_app_id = iosAppId) WHERE id = qrCodeRecord.id; END IF; END LOOP; diff --git a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java index d554013931..978102b443 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java @@ -33,10 +33,9 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.AndroidQrCodeConfig; -import org.thingsboard.server.common.data.mobile.IosQrCodeConfig; import org.thingsboard.server.common.data.mobile.MobileApp; import org.thingsboard.server.common.data.mobile.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.StoreInfo; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.config.annotations.ApiOperation; @@ -101,9 +100,10 @@ public class QrCodeSettingsController extends BaseController { @ApiOperation(value = "Get associated android applications (getAssetLinks)") @GetMapping(value = "/.well-known/assetlinks.json") public ResponseEntity getAssetLinks() { - AndroidQrCodeConfig androidQrConfig = (AndroidQrCodeConfig) qrCodeSettingService.findAppQrCodeConfig(TenantId.SYS_TENANT_ID, ANDROID); - if (androidQrConfig != null && androidQrConfig.isEnabled()) { - return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, androidQrConfig.getAppPackage(), androidQrConfig.getSha256CertFingerprints()))); + MobileApp mobileApp = qrCodeSettingService.findAppFromQrCodeSettings(TenantId.SYS_TENANT_ID, ANDROID); + StoreInfo storeInfo = mobileApp != null ? mobileApp.getStoreInfo() : null; + if (storeInfo != null && storeInfo.isEnabled() && storeInfo.getSha256CertFingerprints() != null) { + return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, mobileApp.getPkgName(), storeInfo.getSha256CertFingerprints()))); } else { return ResponseEntity.notFound().build(); } @@ -112,9 +112,10 @@ public class QrCodeSettingsController extends BaseController { @ApiOperation(value = "Get associated ios applications (getAppleAppSiteAssociation)") @GetMapping(value = "/.well-known/apple-app-site-association") public ResponseEntity getAppleAppSiteAssociation() { - IosQrCodeConfig iosQrCodeConfig = (IosQrCodeConfig) qrCodeSettingService.findAppQrCodeConfig(TenantId.SYS_TENANT_ID, IOS); - if (iosQrCodeConfig != null && iosQrCodeConfig.isEnabled()) { - return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, iosQrCodeConfig.getAppId()))); + MobileApp mobileApp = qrCodeSettingService.findAppFromQrCodeSettings(TenantId.SYS_TENANT_ID, IOS); + StoreInfo storeInfo = mobileApp != null ? mobileApp.getStoreInfo() : null; + if (storeInfo != null && storeInfo.isEnabled() && storeInfo.getAppId() != null) { + return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, storeInfo.getAppId()))); } else { return ResponseEntity.notFound().build(); } @@ -197,7 +198,7 @@ public class QrCodeSettingsController extends BaseController { return null; } MobileApp mobileApp = mobileAppService.findByBundleIdAndPlatformType(TenantId.SYS_TENANT_ID, mobileAppBundleId, platformType); - return (mobileApp != null && mobileApp.getQrCodeConfig() != null) ? mobileApp.getQrCodeConfig().getStoreLink() : null; + return (mobileApp != null && mobileApp.getStoreInfo() != null) ? mobileApp.getStoreInfo().getStoreLink() : null; } } diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java index 3493538266..b957c15804 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java @@ -21,8 +21,6 @@ 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.mobile.AndroidQrCodeConfig; -import org.thingsboard.server.common.data.mobile.IosQrCodeConfig; import org.thingsboard.server.common.data.mobile.MobileApp; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; @@ -83,50 +81,6 @@ public class MobileAppControllerTest extends AbstractControllerTest { .andExpect(statusReason(containsString("appSecret must be at least 16 and max 2048 characters"))); } - @Test - public void testShouldNotSaveMobileAppWithWrongQrCodeConf() throws Exception { - MobileApp mobileApp = validMobileApp("mobileApp.ce", PlatformType.ANDROID); - 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"))); - - 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"))); - - androidQrCodeConfig.setSha256CertFingerprints("test_sha_256"); - androidQrCodeConfig.setStoreLink("https://store.com"); - doPost("/api/mobile/app", mobileApp) - .andExpect(status().isOk()); - } - - @Test - public void testShouldNotSaveMobileAppWithWrongIosConf() throws Exception { - MobileApp mobileApp = validMobileApp("mobileApp.ce", PlatformType.ANDROID); - 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"))); - - iosQrCodeConfig.setAppId("test_app_id"); - iosQrCodeConfig.setStoreLink("https://store.com"); - doPost("/api/mobile/app", mobileApp) - .andExpect(status().isOk()); - } - @Test public void testGetTenantAppsByPlatformTypeSaveMobileApp() throws Exception { MobileApp androidApp = doPost("/api/mobile/app", validMobileApp("android.1", PlatformType.ANDROID), MobileApp.class); diff --git a/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java index b359c8b500..2ea9344f3d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java @@ -23,13 +23,12 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Value; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.mobile.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.QRCodeConfig; import org.thingsboard.server.common.data.mobile.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.StoreInfo; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -68,22 +67,21 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { loginSysAdmin(); MobileApp androidApp = validMobileApp( "my.android.package", PlatformType.ANDROID); - AndroidQrCodeConfig androidQrCodeConfig = AndroidQrCodeConfig.builder() - .appPackage(ANDROID_PACKAGE_NAME) + StoreInfo androidStoreInfo = StoreInfo.builder() .sha256CertFingerprints(ANDROID_APP_SHA256) .storeLink(ANDROID_STORE_LINK) .enabled(true) .build(); - androidApp.setQrCodeConfig(androidQrCodeConfig); + androidApp.setStoreInfo(androidStoreInfo); MobileApp savedAndroidApp = doPost("/api/mobile/app", androidApp, MobileApp.class); MobileApp iosApp = validMobileApp( "my.ios.package", PlatformType.IOS); - IosQrCodeConfig iosQrCodeConfig = IosQrCodeConfig.builder() + StoreInfo iosQrCodeConfig = StoreInfo.builder() .appId(APPLE_APP_ID) .enabled(true) .storeLink(IOS_STORE_LINK) .build(); - iosApp.setQrCodeConfig(iosQrCodeConfig); + iosApp.setStoreInfo(iosQrCodeConfig); MobileApp savedIosApp = doPost("/api/mobile/app", iosApp, MobileApp.class); mobileAppBundle = new MobileAppBundle(); @@ -180,7 +178,7 @@ public class QrCodeSettingsControllerTest extends AbstractControllerTest { .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("package_name").asText()).isEqualTo("my.android.package"); assertThat(assetLinks.get(0).get("target").get("sha256_cert_fingerprints").get(0).asText()).isEqualTo(ANDROID_APP_SHA256); JsonNode appleAssociation = doGet("/.well-known/apple-app-site-association", JsonNode.class); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidQrCodeConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidQrCodeConfig.java deleted file mode 100644 index 01e4845702..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidQrCodeConfig.java +++ /dev/null @@ -1,49 +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.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 -@Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -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; - } -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java index b4475a0c50..2e23547492 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java @@ -55,9 +55,9 @@ public class MobileApp extends BaseData implements HasTenantId, Has @Schema(description = "Application version info") @Valid private MobileAppVersionInfo versionInfo; - @Schema(description = "Application qr code configuration") + @Schema(description = "Application store information") @Valid - private QrCodeConfig qrCodeConfig; + private StoreInfo storeInfo; public MobileApp() { super(); @@ -75,7 +75,7 @@ public class MobileApp extends BaseData implements HasTenantId, Has this.platformType = mobile.platformType; this.status = mobile.status; this.versionInfo = mobile.versionInfo; - this.qrCodeConfig = mobile.qrCodeConfig; + this.storeInfo = mobile.storeInfo; } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeConfig.java deleted file mode 100644 index 167b2c6a91..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeConfig.java +++ /dev/null @@ -1,38 +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.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(); - - String getStoreLink(); - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosQrCodeConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/StoreInfo.java similarity index 67% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosQrCodeConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/StoreInfo.java index 84ed6c726e..5ae4023c23 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosQrCodeConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/StoreInfo.java @@ -15,32 +15,20 @@ */ 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 @Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -public class IosQrCodeConfig implements QrCodeConfig { +public class StoreInfo { private boolean enabled; @NoXss - @NotBlank private String appId; @NoXss - @NotBlank + private String sha256CertFingerprints; + @NoXss private String storeLink; - @Override - public PlatformType getType() { - return PlatformType.IOS; - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java index 9c5a19c175..0da5115f62 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java @@ -16,7 +16,7 @@ package org.thingsboard.server.dao.mobile; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.QrCodeConfig; +import org.thingsboard.server.common.data.mobile.MobileApp; import org.thingsboard.server.common.data.mobile.QrCodeSettings; import org.thingsboard.server.common.data.oauth2.PlatformType; @@ -26,7 +26,7 @@ public interface QrCodeSettingService { QrCodeSettings findQrCodeSettings(TenantId tenantId); - QrCodeConfig findAppQrCodeConfig(TenantId sysTenantId, PlatformType platformType); + MobileApp findAppFromQrCodeSettings(TenantId sysTenantId, PlatformType platformType); void deleteByTenantId(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java index 4cebb26451..a31c830748 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java @@ -22,7 +22,7 @@ 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.BadgePosition; -import org.thingsboard.server.common.data.mobile.QrCodeConfig; +import org.thingsboard.server.common.data.mobile.MobileApp; import org.thingsboard.server.common.data.mobile.QrCodeSettings; import org.thingsboard.server.common.data.mobile.QRCodeConfig; import org.thingsboard.server.common.data.oauth2.PlatformType; @@ -75,10 +75,10 @@ public class QrCodeSettingServiceImpl extends AbstractCachedEntityService { this.platformType = mobile.getPlatformType(); this.status = mobile.getStatus(); this.versionInfo = toJson(mobile.getVersionInfo()); - this.qrCodeConfig = toJson(mobile.getQrCodeConfig()); + this.qrCodeConfig = toJson(mobile.getStoreInfo()); } @Override @@ -100,7 +100,7 @@ public class MobileAppEntity extends BaseSqlEntity { mobile.setPlatformType(platformType); mobile.setStatus(status); mobile.setVersionInfo(fromJson(versionInfo, MobileAppVersionInfo.class)); - mobile.setQrCodeConfig(fromJson(qrCodeConfig, QrCodeConfig.class)); + mobile.setStoreInfo(fromJson(qrCodeConfig, StoreInfo.class)); return mobile; } } From 5790ee16426e8cd17a62a17b1dd5fca59b4516ec Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 11 Oct 2024 11:24:39 +0300 Subject: [PATCH 09/48] UI: Add mobile center menu item and implements application page --- .../common/data/mobile/MobileAppStatus.java | 1 + .../src/app/core/http/mobile-app.service.ts | 36 +++-- .../core/http/mobile-application.service.ts | 12 +- ui-ngx/src/app/core/services/menu.models.ts | 68 +++++--- .../entity/entities-table.component.html | 4 +- .../entity/entities-table.component.scss | 11 ++ .../lib/mobile-app-qrcode-widget.component.ts | 8 +- .../admin/mobile-app-settings.component.ts | 6 +- .../mobile-app-table-config.resolver.ts | 143 ---------------- .../mobile-apps/mobile-app.component.html | 76 --------- .../mobile-apps/mobile-app.component.ts | 106 ------------ .../admin/oauth2/oauth2-routing.module.ts | 18 +-- .../home/pages/admin/oauth2/oauth2.module.ts | 6 +- .../modules/home/pages/home-pages.module.ts | 2 + .../applications-routing.module.ts | 84 ++++++++++ .../applications/applications.module.ts | 37 +++++ .../mobile-app-table-config.resolver.ts | 151 +++++++++++++++++ .../mobile-app-table-header.component.html | 12 +- .../mobile-app-table-header.component.scss | 23 +++ .../mobile-app-table-header.component.ts | 10 +- .../applications/mobile-app.component.html | 145 +++++++++++++++++ .../applications/mobile-app.component.scss | 18 +++ .../applications/mobile-app.component.ts | 153 ++++++++++++++++++ .../pages/mobile/mobile-routing.module.ts | 78 +++++++++ .../home/pages/mobile/mobile.module.ts | 33 ++++ .../app/shared/models/entity-type.models.ts | 7 +- .../shared/models/id/mobile-app-bundle-id.ts | 27 ++++ ui-ngx/src/app/shared/models/id/public-api.ts | 2 + .../app/shared/models/mobile-app.models.ts | 95 ++++++++++- ui-ngx/src/app/shared/models/oauth2.models.ts | 12 -- .../assets/locale/locale.constant-en_US.json | 45 ++++++ 31 files changed, 1008 insertions(+), 421 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts delete mode 100644 ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html delete mode 100644 ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/applications-routing.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts rename ui-ngx/src/app/modules/home/pages/{admin/oauth2/mobile-apps => mobile/applications}/mobile-app-table-header.component.html (53%) create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.scss rename ui-ngx/src/app/modules/home/pages/{admin/oauth2/mobile-apps => mobile/applications}/mobile-app-table-header.component.ts (82%) create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts create mode 100644 ui-ngx/src/app/shared/models/id/mobile-app-bundle-id.ts diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppStatus.java index f1043c732d..3ffe9cbf10 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppStatus.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppStatus.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.mobile; public enum MobileAppStatus { + DRAFT, PUBLISHED, DEPRECATED, SUSPENDED diff --git a/ui-ngx/src/app/core/http/mobile-app.service.ts b/ui-ngx/src/app/core/http/mobile-app.service.ts index a506d2805e..ac4756f26f 100644 --- a/ui-ngx/src/app/core/http/mobile-app.service.ts +++ b/ui-ngx/src/app/core/http/mobile-app.service.ts @@ -18,9 +18,9 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { Observable } from 'rxjs'; -import { MobileApp, MobileAppInfo } from '@shared/models/oauth2.models'; import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; +import { MobileApp, MobileAppBundle, MobileAppBundleInfo } from '@shared/models/mobile-app.models'; @Injectable({ providedIn: 'root' @@ -32,25 +32,37 @@ export class MobileAppService { ) { } - public saveMobileApp(mobileApp: MobileApp, oauth2ClientIds: Array, config?: RequestConfig): Observable { - return this.http.post(`/api/mobileApp?oauth2ClientIds=${oauth2ClientIds.join(',')}`, - mobileApp, defaultHttpOptionsFromConfig(config)); + public saveMobileApp(mobileApp: MobileApp, config?: RequestConfig): Observable { + return this.http.post(`/api/mobile/app`, mobileApp, defaultHttpOptionsFromConfig(config)); } - public updateOauth2Clients(id: string, oauth2ClientRegistrationIds: Array, config?: RequestConfig): Observable { - return this.http.put(`/api/mobileApp/${id}/oauth2Clients`, oauth2ClientRegistrationIds, defaultHttpOptionsFromConfig(config)); + public getTenantMobileAppInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/mobile/app${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } - public getTenantMobileAppInfos(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/mobileApp/infos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + public getMobileAppInfoById(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/app/${id}`, defaultHttpOptionsFromConfig(config)); } - public getMobileAppInfoById(id: string, config?: RequestConfig): Observable { - return this.http.get(`/api/mobileApp/info/${id}`, defaultHttpOptionsFromConfig(config)); + public deleteMobileApp(id: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/mobile/app/${id}`, defaultHttpOptionsFromConfig(config)); } - public deleteMobileApp(id: string, config?: RequestConfig): Observable { - return this.http.delete(`/api/mobileApp/${id}`, defaultHttpOptionsFromConfig(config)); + public saveMobileAppBundle(mobileAppBundle: MobileAppBundle, oauth2ClientIds?: Array, config?: RequestConfig) { + return this.http.post(`/api/mobile/bundle${oauth2ClientIds ? '?oauth2ClientIds=' + oauth2ClientIds.join(',') : ''}`, + mobileAppBundle, defaultHttpOptionsFromConfig(config)); + } + + public updateOauth2Clients(id: string, oauth2ClientIds?: Array, config?: RequestConfig) { + return this.http.put(`/mobile/bundle/${id}/oauth2Clients`, oauth2ClientIds, defaultHttpOptionsFromConfig(config)); + } + + public getTenantMobileAppBundleInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/mobile/bundle/infos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getMobileAppBundleInfoById(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/bundle/infos/${id}`, defaultHttpOptionsFromConfig(config)); } } diff --git a/ui-ngx/src/app/core/http/mobile-application.service.ts b/ui-ngx/src/app/core/http/mobile-application.service.ts index 17ebdd3fef..3dfbf8a4bf 100644 --- a/ui-ngx/src/app/core/http/mobile-application.service.ts +++ b/ui-ngx/src/app/core/http/mobile-application.service.ts @@ -18,7 +18,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { Observable } from 'rxjs'; -import { MobileAppSettings } from '@shared/models/mobile-app.models'; +import { QrCodeSettings } from '@shared/models/mobile-app.models'; @Injectable({ providedIn: 'root' @@ -29,16 +29,16 @@ export class MobileApplicationService { private http: HttpClient ) {} - public getMobileAppSettings(config?: RequestConfig): Observable { - return this.http.get(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); + public getMobileAppSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/qr/settings`, defaultHttpOptionsFromConfig(config)); } - public saveMobileAppSettings(mobileAppSettings: MobileAppSettings, config?: RequestConfig): Observable { - return this.http.post(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); + public saveMobileAppSettings(mobileAppSettings: QrCodeSettings, config?: RequestConfig): Observable { + return this.http.post(`/api/mobile/qr/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); } public getMobileAppDeepLink(config?: RequestConfig): Observable { - return this.http.get(`/api/mobile/deepLink`, defaultHttpOptionsFromConfig(config)); + return this.http.get(`/api/mobile/qr/deepLink`, defaultHttpOptionsFromConfig(config)); } } diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index ee4c36b6f2..debaefe882 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -65,6 +65,9 @@ export enum MenuId { notification_recipients = 'notification_recipients', notification_templates = 'notification_templates', notification_rules = 'notification_rules', + mobile_center = 'mobile_center', + mobile_apps = 'mobile_apps', + mobile_app_settings = 'mobile_app_settings', settings = 'settings', general = 'general', mail_server = 'mail_server', @@ -73,13 +76,11 @@ export enum MenuId { repository_settings = 'repository_settings', auto_commit_settings = 'auto_commit_settings', queues = 'queues', - mobile_app_settings = 'mobile_app_settings', security_settings = 'security_settings', security_settings_general = 'security_settings_general', two_fa = 'two_fa', oauth2 = 'oauth2', domains = 'domains', - mobile_apps = 'mobile_apps', clients = 'clients', audit_log = 'audit_log', alarms = 'alarms', @@ -271,6 +272,37 @@ export const menuSectionMap = new Map([ icon: 'mdi:message-cog' } ], + [ + MenuId.mobile_center, + { + id: MenuId.mobile_center, + name: 'mobile.mobile-center', + type: 'link', + path: '/mobile-center', + icon: 'smartphone' + } + ], + [ + MenuId.mobile_apps, + { + id: MenuId.mobile_apps, + name: 'mobile.applications', + type: 'link', + path: '/mobile-center/applications', + icon: 'list' + } + ], + [ + MenuId.mobile_app_settings, + { + id: MenuId.mobile_app_settings, + name: 'admin.mobile-app.mobile-app', + fullName: 'admin.mobile-app.mobile-app', + type: 'link', + path: '/mobile-center/mobile-app', + icon: 'smartphone' + } + ], [ MenuId.settings, { @@ -356,17 +388,6 @@ export const menuSectionMap = new Map([ icon: 'swap_calls' } ], - [ - MenuId.mobile_app_settings, - { - id: MenuId.mobile_app_settings, - name: 'admin.mobile-app.mobile-app', - fullName: 'admin.mobile-app.mobile-app', - type: 'link', - path: '/settings/mobile-app', - icon: 'smartphone' - } - ], [ MenuId.security_settings, { @@ -418,16 +439,6 @@ export const menuSectionMap = new Map([ icon: 'domain' } ], - [ - MenuId.mobile_apps, - { - id: MenuId.mobile_apps, - name: 'admin.oauth2.mobile-apps', - type: 'link', - path: '/security-settings/oauth2/mobile-applications', - icon: 'smartphone' - } - ], [ MenuId.clients, { @@ -687,14 +698,20 @@ const defaultUserMenuMap = new Map([ {id: MenuId.notification_rules} ] }, + { + id: MenuId.mobile_center, + pages: [ + {id: MenuId.mobile_apps}, + {id: MenuId.mobile_app_settings} + ] + }, { id: MenuId.settings, pages: [ {id: MenuId.general}, {id: MenuId.mail_server}, {id: MenuId.notification_settings}, - {id: MenuId.queues}, - {id: MenuId.mobile_app_settings} + {id: MenuId.queues} ] }, { @@ -706,7 +723,6 @@ const defaultUserMenuMap = new Map([ id: MenuId.oauth2, pages: [ {id: MenuId.domains}, - {id: MenuId.mobile_apps}, {id: MenuId.clients} ] } diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index 9bf9443404..1700938611 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -159,7 +159,8 @@ [fxHide.lt-lg]="column.mobileHide" *matHeaderCellDef [ngStyle]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable"> {{ column.ignoreTranslate ? column.title : (column.title | translate) }} - (); private widgetResize$: ResizeObserver; - private mobileAppSettingsValue: MobileAppSettings; + private mobileAppSettingsValue: QrCodeSettings; private deepLink: string; private deepLinkTTL: number; private deepLinkTTLTimeoutID: NodeJS.Timeout; @@ -65,13 +65,13 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI widgetTitlePanel: TemplateRef; @Input() - set mobileAppSettings(settings: MobileAppSettings) { + set mobileAppSettings(settings: QrCodeSettings) { if (settings) { this.mobileAppSettingsValue = settings; } }; - get mobileAppSettings(): MobileAppSettings { + get mobileAppSettings(): QrCodeSettings { return this.mobileAppSettingsValue; } diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts index 7f2a48253f..d50a888d4a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts @@ -25,7 +25,7 @@ import { MobileApplicationService } from '@core/http/mobile-application.service' import { BadgePosition, badgePositionTranslationsMap, - MobileAppSettings + QrCodeSettings } from '@shared/models/mobile-app.models'; import { ActionUpdateMobileQrCodeEnabled } from '@core/auth/auth.actions'; @@ -38,7 +38,7 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf mobileAppSettingsForm: FormGroup; - mobileAppSettings: MobileAppSettings; + mobileAppSettings: QrCodeSettings; private readonly destroy$ = new Subject(); @@ -150,7 +150,7 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf }); } - private processMobileAppSettings(mobileAppSettings: MobileAppSettings): void { + private processMobileAppSettings(mobileAppSettings: QrCodeSettings): void { this.mobileAppSettings = {...mobileAppSettings}; this.mobileAppSettingsForm.reset(this.mobileAppSettings); } diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts deleted file mode 100644 index 1f81196d11..0000000000 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts +++ /dev/null @@ -1,143 +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. -/// - -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot } from '@angular/router'; -import { - CellActionDescriptorType, - DateEntityTableColumn, - EntityActionTableColumn, - EntityChipsEntityTableColumn, - EntityTableColumn, - EntityTableConfig -} from '@home/models/entity/entities-table-config.models'; -import { MobileApp, MobileAppInfo } from '@shared/models/oauth2.models'; -import { TranslateService } from '@ngx-translate/core'; -import { DatePipe } from '@angular/common'; -import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; -import { isEqual } from '@core/utils'; -import { Direction } from '@app/shared/models/page/sort-order'; -import { MobileAppService } from '@core/http/mobile-app.service'; -import { MobileAppComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app.component'; -import { MobileAppTableHeaderComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component'; -import { map, Observable, of, mergeMap } from 'rxjs'; - -@Injectable() -export class MobileAppTableConfigResolver { - - private readonly config: EntityTableConfig = new EntityTableConfig(); - - constructor(private translate: TranslateService, - private datePipe: DatePipe, - private mobileAppService: MobileAppService) { - this.config.tableTitle = this.translate.instant('admin.oauth2.mobile-apps'); - this.config.selectionEnabled = false; - this.config.entityType = EntityType.MOBILE_APP; - this.config.rowPointer = true; - this.config.entityTranslations = entityTypeTranslations.get(EntityType.MOBILE_APP); - this.config.entityResources = entityTypeResources.get(EntityType.MOBILE_APP); - this.config.entityComponent = MobileAppComponent; - this.config.headerComponent = MobileAppTableHeaderComponent; - this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'}; - this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; - - this.config.columns.push( - new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), - new EntityTableColumn('pkgName', 'admin.oauth2.mobile-package', '170px'), - new EntityTableColumn('appSecret', 'admin.oauth2.mobile-app-secret', '350px', - (entity) => entity.appSecret ? this.appSecretText(entity) : '', () => ({}), - false, () => ({}), () => undefined, false, - { - name: this.translate.instant('admin.oauth2.copy-mobile-app-secret'), - icon: 'content_copy', - style: { - padding: '4px', - 'font-size': '16px', - color: 'rgba(0,0,0,.87)' - }, - isEnabled: (entity) => !!entity.appSecret, - onAction: ($event, entity) => entity.appSecret, - type: CellActionDescriptorType.COPY_BUTTON - }), - new EntityChipsEntityTableColumn('oauth2ClientInfos', 'admin.oauth2.clients', '20%'), - new EntityActionTableColumn('oauth2Enabled', 'admin.oauth2.enable', - { - name: '', - nameFunction: (app) => - this.translate.instant(app.oauth2Enabled ? 'admin.oauth2.disable' : 'admin.oauth2.enable'), - icon: 'mdi:toggle-switch', - iconFunction: (entity) => entity.oauth2Enabled ? 'mdi:toggle-switch' : 'mdi:toggle-switch-off-outline', - isEnabled: () => true, - onAction: ($event, entity) => this.toggleEnableOAuth($event, entity) - }) - ); - - this.config.deleteEntityTitle = (app) => this.translate.instant('admin.oauth2.delete-mobile-app-title', {applicationName: app.pkgName}); - this.config.deleteEntityContent = () => this.translate.instant('admin.oauth2.delete-mobile-app-text'); - this.config.entitiesFetchFunction = pageLink => this.mobileAppService.getTenantMobileAppInfos(pageLink); - this.config.loadEntity = id => this.mobileAppService.getMobileAppInfoById(id.id); - this.config.saveEntity = (mobileApp, originalMobileApp) => { - const clientsIds = mobileApp.oauth2ClientInfos as Array || []; - let clientsTask: Observable; - if (mobileApp.id && !isEqual(mobileApp.oauth2ClientInfos?.sort(), - originalMobileApp.oauth2ClientInfos?.map(info => info.id ? info.id.id : info).sort())) { - clientsTask = this.mobileAppService.updateOauth2Clients(mobileApp.id.id, clientsIds); - } else { - clientsTask = of(null); - } - delete mobileApp.oauth2ClientInfos; - return clientsTask.pipe( - mergeMap(() => this.mobileAppService.saveMobileApp(mobileApp as MobileApp, mobileApp.id ? [] : clientsIds)), - map(savedMobileApp => { - (savedMobileApp as MobileAppInfo).oauth2ClientInfos = clientsIds; - return savedMobileApp; - }) - ); - }; - this.config.deleteEntity = id => this.mobileAppService.deleteMobileApp(id.id); - } - - resolve(route: ActivatedRouteSnapshot): EntityTableConfig { - return this.config; - } - - private toggleEnableOAuth($event: Event, mobileApp: MobileAppInfo): void { - if ($event) { - $event.stopPropagation(); - } - - const modifiedMobileApp: MobileAppInfo = { - ...mobileApp, - oauth2Enabled: !mobileApp.oauth2Enabled - }; - - this.mobileAppService.saveMobileApp(modifiedMobileApp, mobileApp.oauth2ClientInfos.map(clientInfo => clientInfo.id.id), - {ignoreLoading: true}) - .subscribe((result) => { - mobileApp.oauth2Enabled = result.oauth2Enabled; - this.config.getTable().detectChanges(); - }); - } - - private appSecretText(entity): string { - let text = entity.appSecret; - if (text.length > 35) { - text = `${text.slice(0, 35)}…`; - } - return text; - } - -} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html deleted file mode 100644 index a613890c95..0000000000 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html +++ /dev/null @@ -1,76 +0,0 @@ - -
-
- - admin.oauth2.mobile-package - - admin.oauth2.mobile-package-hint - - {{ 'admin.oauth2.mobile-package-unique' | translate }} - - - {{ 'admin.oauth2.mobile-package-max-length' | translate }} - - - {{ 'admin.oauth2.mobile-package-spaces' | translate }} - - -
-
- - admin.oauth2.mobile-app-secret - - - - admin.oauth2.mobile-app-secret-hint - - {{ 'admin.oauth2.mobile-app-secret-required' | translate }} - - - {{ 'admin.oauth2.mobile-app-secret-min-length' | translate }} - - - {{ 'admin.oauth2.mobile-app-secret-base64' | translate }} - - -
-
- - {{ 'admin.oauth2.enable' | translate }} - -
- - - -
- diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts deleted file mode 100644 index a01699bdda..0000000000 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts +++ /dev/null @@ -1,106 +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. -/// - - -import { ChangeDetectorRef, Component, Inject } from '@angular/core'; -import { EntityComponent } from '@home/components/entity/entity.component'; -import { MobileAppInfo } from '@shared/models/oauth2.models'; -import { AppState } from '@core/core.state'; -import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import { TranslateService } from '@ngx-translate/core'; -import { Store } from '@ngrx/store'; -import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; -import { isDefinedAndNotNull, randomAlphanumeric } from '@core/utils'; -import { MatDialog } from '@angular/material/dialog'; -import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component'; -import { EntityType } from '@shared/models/entity-type.models'; - -@Component({ - selector: 'tb-mobile-app', - templateUrl: './mobile-app.component.html', - styleUrls: [] -}) -export class MobileAppComponent extends EntityComponent { - - entityType = EntityType; - - constructor(protected store: Store, - protected translate: TranslateService, - @Inject('entity') protected entityValue: MobileAppInfo, - @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, - protected cd: ChangeDetectorRef, - public fb: UntypedFormBuilder, - private dialog: MatDialog) { - super(store, fb, entityValue, entitiesTableConfigValue, cd); - } - - buildForm(entity: MobileAppInfo): UntypedFormGroup { - return this.fb.group({ - pkgName: [entity?.pkgName ? entity.pkgName : '', [Validators.required, Validators.maxLength(255), - Validators.pattern(/^\S+$/)]], - appSecret: [entity?.appSecret ? entity.appSecret : btoa(randomAlphanumeric(64)), - [Validators.required, this.base64Format]], - oauth2Enabled: isDefinedAndNotNull(entity?.oauth2Enabled) ? entity.oauth2Enabled : true, - oauth2ClientInfos: entity?.oauth2ClientInfos ? entity.oauth2ClientInfos.map(info => info.id.id) : [] - }); - } - - updateForm(entity: MobileAppInfo) { - this.entityForm.patchValue({ - pkgName: entity.pkgName, - appSecret: entity.appSecret, - oauth2Enabled: entity.oauth2Enabled, - oauth2ClientInfos: entity.oauth2ClientInfos?.map(info => info.id ? info.id.id : info) - }); - } - - createClient($event: Event) { - if ($event) { - $event.stopPropagation(); - $event.preventDefault(); - } - this.dialog.open(ClientDialogComponent, { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: {} - }).afterClosed() - .subscribe((client) => { - if (client) { - const formValue = this.entityForm.get('oauth2ClientInfos').value ? - [...this.entityForm.get('oauth2ClientInfos').value] : []; - formValue.push(client.id.id); - this.entityForm.get('oauth2ClientInfos').patchValue(formValue); - this.entityForm.get('oauth2ClientInfos').markAsDirty(); - } - }); - } - - private base64Format(control: UntypedFormControl): { [key: string]: boolean } | null { - if (control.value === '') { - return null; - } - try { - const value = atob(control.value); - if (value.length < 64) { - return {minLength: true}; - } - return null; - } catch (e) { - return {base64: true}; - } - } - -} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts index cba52e977c..9286304312 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts @@ -26,7 +26,6 @@ import { DomainTableConfigResolver } from '@home/pages/admin/oauth2/domains/doma import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; -import { MobileAppTableConfigResolver } from '@home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver'; import { MenuId } from '@core/services/menu.models'; @Injectable() @@ -74,20 +73,6 @@ export const oAuth2Routes: Routes = [ entitiesTableConfig: DomainTableConfigResolver } }, - { - path: 'mobile-applications', - component: EntitiesTableComponent, - data: { - auth: [Authority.SYS_ADMIN], - title: 'admin.oauth2.mobile-apps', - breadcrumb: { - menuId: MenuId.mobile_apps - } - }, - resolve: { - entitiesTableConfig: MobileAppTableConfigResolver - } - }, { path: 'clients', data: { @@ -140,8 +125,7 @@ export const oAuth2Routes: Routes = [ providers: [ OAuth2LoginProcessingUrlResolver, ClientsTableConfigResolver, - DomainTableConfigResolver, - MobileAppTableConfigResolver + DomainTableConfigResolver ], imports: [RouterModule.forChild(oAuth2Routes)], exports: [RouterModule] diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2.module.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2.module.ts index 545b9a6a63..8c80d6775a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2.module.ts @@ -24,8 +24,6 @@ import { ClientTableHeaderComponent } from '@home/pages/admin/oauth2/clients/cli import { DomainComponent } from '@home/pages/admin/oauth2/domains/domain.component'; import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component'; import { DomainTableHeaderComponent } from '@home/pages/admin/oauth2/domains/domain-table-header.component'; -import { MobileAppComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app.component'; -import { MobileAppTableHeaderComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component'; @NgModule({ declarations: [ @@ -33,9 +31,7 @@ import { MobileAppTableHeaderComponent } from '@home/pages/admin/oauth2/mobile-a ClientDialogComponent, ClientTableHeaderComponent, DomainComponent, - DomainTableHeaderComponent, - MobileAppComponent, - MobileAppTableHeaderComponent + DomainTableHeaderComponent ], imports: [ Oauth2RoutingModule, diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts index 0b1a70996e..cd930064b6 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -44,6 +44,7 @@ import { FeaturesModule } from '@home/pages/features/features.module'; import { NotificationModule } from '@home/pages/notification/notification.module'; import { AccountModule } from '@home/pages/account/account.module'; import { ScadaSymbolModule } from '@home/pages/scada-symbol/scada-symbol.module'; +import { MobileModule } from '@home/pages/mobile/mobile.module'; @NgModule({ exports: [ @@ -58,6 +59,7 @@ import { ScadaSymbolModule } from '@home/pages/scada-symbol/scada-symbol.module' ProfilesModule, EntitiesModule, FeaturesModule, + MobileModule, NotificationModule, DeviceModule, AssetModule, diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications-routing.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications-routing.module.ts new file mode 100644 index 0000000000..59e6917371 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications-routing.module.ts @@ -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. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { Authority } from '@shared/models/authority.enum'; +import { MenuId } from '@core/services/menu.models'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { MobileAppTableConfigResolver } from '@home/pages/mobile/applications/mobile-app-table-config.resolver'; +import { DevicesTableConfigResolver } from '@home/pages/device/devices-table-config.resolver'; +import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; +import { BreadCrumbConfig } from '@shared/components/breadcrumb'; + +export const applicationsRoutes: Routes = [ + { + path: 'applications', + data: { + breadcrumb: { + menuId: MenuId.mobile_apps + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'mobile.applications', + }, + resolve: { + entitiesTableConfig: MobileAppTableConfigResolver + } + }, + { + path: ':entityId', + component: EntityDetailsPageComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: entityDetailsPageBreadcrumbLabelFunction, + icon: 'list' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'mobile.applications', + }, + resolve: { + entitiesTableConfig: MobileAppTableConfigResolver + } + } + ] + } +]; + +const routes: Routes = [ + { + path: 'security-settings/oauth2/mobile-applications', + pathMatch: 'full', + redirectTo: '/mobile-center/applications' + } +]; + +@NgModule({ + providers: [ + MobileAppTableConfigResolver + ], + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class ApplicationsRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts new file mode 100644 index 0000000000..c213ab9ee6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts @@ -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. +/// + +import { NgModule } from '@angular/core'; +import { MobileAppComponent } from '@home/pages/mobile/applications/mobile-app.component'; +import { MobileAppTableHeaderComponent } from '@home/pages/mobile/applications/mobile-app-table-header.component'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeComponentsModule } from '@home/components/home-components.module'; +import { ApplicationsRoutingModule } from '@home/pages/mobile/applications/applications-routing.module'; + +@NgModule({ + declarations: [ + MobileAppComponent, + MobileAppTableHeaderComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + ApplicationsRoutingModule + ] +}) +export class ApplicationModule { } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts new file mode 100644 index 0000000000..2c86f6124c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts @@ -0,0 +1,151 @@ +/// +/// 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. +/// + +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; +import { + CellActionDescriptorType, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { Direction } from '@app/shared/models/page/sort-order'; +import { MobileAppService } from '@core/http/mobile-app.service'; +import { MobileAppComponent } from '@home/pages/mobile/applications/mobile-app.component'; +import { MobileAppTableHeaderComponent } from '@home/pages/mobile/applications/mobile-app-table-header.component'; +import { MobileApp, MobileAppStatus, mobileAppStatusTranslations } from '@shared/models/mobile-app.models'; +import { platformTypeTranslations } from '@shared/models/oauth2.models'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; + +@Injectable() +export class MobileAppTableConfigResolver { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private translate: TranslateService, + private datePipe: DatePipe, + private mobileAppService: MobileAppService, + private truncatePipe: TruncatePipe) { + this.config.selectionEnabled = false; + this.config.entityType = EntityType.MOBILE_APP; + this.config.addEnabled = false; + this.config.rowPointer = true; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.MOBILE_APP); + this.config.entityResources = entityTypeResources.get(EntityType.MOBILE_APP); + this.config.entityComponent = MobileAppComponent; + this.config.headerComponent = MobileAppTableHeaderComponent; + this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'}; + this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('pkgName', 'mobile.application-package', '20%', (entity) => entity.pkgName ?? '', () => ({}), + false, () => ({}), () => undefined, false, + { + name: this.translate.instant('mobile.copy-application-package'), + icon: 'content_copy', + style: { + padding: '4px', + 'font-size': '16px', + color: 'rgba(0,0,0,.54)' + }, + isEnabled: (entity) => !!entity.pkgName, + onAction: (_$event, entity) => entity.pkgName, + type: CellActionDescriptorType.COPY_BUTTON + }), + new EntityTableColumn('appSecret', 'mobile.application-secret', '15%', + (entity) => this.truncatePipe.transform(entity.appSecret, true, 10, '…'), () => ({}), + false, () => ({}), () => undefined, false, + { + name: this.translate.instant('mobile.copy-application-secret'), + icon: 'content_copy', + style: { + padding: '4px', + 'font-size': '16px', + color: 'rgba(0,0,0,.54)' + }, + isEnabled: (entity) => !!entity.appSecret, + onAction: (_$event, entity) => entity.appSecret, + type: CellActionDescriptorType.COPY_BUTTON + }), + new EntityTableColumn('platformType', 'mobile.platform-type', '15%', + (entity) => this.translate.instant(platformTypeTranslations.get(entity.platformType)) + ), + new EntityTableColumn('status', 'mobile.status', '15%', + (entity) => `${this.mobileStatus(entity.status)}`, + (entity)=> this.mobileStatusStyle(entity.status) + ), + new EntityTableColumn('minVersion', 'mobile.min-version', '15%', + (entity) => entity.versionInfo?.minVersion ?? '', () => ({}), false), + new EntityTableColumn('latestVersion', 'mobile.latest-version', '15%', + (entity) => entity.versionInfo?.latestVersion ?? '', () => ({}), false), + ); + + this.config.deleteEntityTitle = (app) => this.translate.instant('mobile.delete-applications-title', {applicationName: app.pkgName}); + this.config.deleteEntityContent = () => this.translate.instant('mobile.delete-applications-text'); + this.config.entitiesFetchFunction = pageLink => this.mobileAppService.getTenantMobileAppInfos(pageLink); + this.config.loadEntity = id => this.mobileAppService.getMobileAppInfoById(id.id); + this.config.saveEntity = (mobileApp) => this.mobileAppService.saveMobileApp(mobileApp); + this.config.deleteEntity = id => this.mobileAppService.deleteMobileApp(id.id); + } + + resolve(_route: ActivatedRouteSnapshot): EntityTableConfig { + return this.config; + } + + private mobileStatus(status: MobileAppStatus): string { + const translateKey = mobileAppStatusTranslations.get(status); + let backgroundColor = 'rgba(25, 128, 56, 0.06)'; + switch (status) { + case MobileAppStatus.DEPRECATED: + backgroundColor = 'rgba(250, 164, 5, 0.06)'; + break; + case MobileAppStatus.SUSPENDED: + backgroundColor = 'rgba(209, 39, 48, 0.06)'; + break; + case MobileAppStatus.DRAFT: + backgroundColor = 'rgba(160, 160, 160, 0.06)'; + break; + } + return `
+ ${this.translate.instant(translateKey)} +
`; + } + + private mobileStatusStyle(status: MobileAppStatus): object { + const styleObj = { + fontSize: '14px', + color: '#198038' + }; + switch (status) { + case MobileAppStatus.DEPRECATED: + styleObj.color = '#FAA405'; + break; + case MobileAppStatus.SUSPENDED: + styleObj.color = '#D12730'; + break; + case MobileAppStatus.DRAFT: + styleObj.color = '#A0A0A0'; + break; + } + return styleObj; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.html similarity index 53% rename from ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.html rename to ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.html index fd66a026da..1783b2b4dd 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.html @@ -15,4 +15,14 @@ limitations under the License. --> -
+
+
+
mobile.applications
+ +
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.scss new file mode 100644 index 0000000000..06f14e4712 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.scss @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host{ + width: 100000px; + + .tb-entity-title-container { + display: flex; + align-items: center; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.ts similarity index 82% rename from ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.ts rename to ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.ts index ca4bbaf74b..a5ced5ddb8 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.ts @@ -18,16 +18,20 @@ import { Component } from '@angular/core'; import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { MobileAppInfo } from '@shared/models/oauth2.models'; +import { MobileApp } from '@shared/models/mobile-app.models'; @Component({ selector: 'tb-mobile-app-table-header', templateUrl: './mobile-app-table-header.component.html', - styleUrls: [] + styleUrls: ['./mobile-app-table-header.component.scss'] }) -export class MobileAppTableHeaderComponent extends EntityTableHeaderComponent { +export class MobileAppTableHeaderComponent extends EntityTableHeaderComponent { constructor(protected store: Store) { super(store); } + + createMobile($event: Event) { + this.entitiesTableConfig.getTable().addEntity($event); + } } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html new file mode 100644 index 0000000000..bceaa4048f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html @@ -0,0 +1,145 @@ + +
+ + mobile.mobile-package + + + + + + {{ 'mobile.mobile-package-required' | translate }} + + + {{ 'mobile.mobile-package-max-length' | translate }} + + + {{ 'mobile.mobile-package-spaces' | translate }} + + + + mobile.platform-type + + + {{ platformTypeTranslations.get(platformType) | translate }} + + + + + mobile.application-secret + +
+ + + +
+ +
+ + mobile.status + + + {{ mobileAppStatusTranslations.get(mobileAppStatus) | translate }} + + + +
+
mobile.version-information
+
+ + mobile.min-version + + + + mobile.latest-version + + +
+
+
+
mobile.store-information
+
+ + {{ + (entityForm.get('platformType').value === PlatformType.ANDROID ? 'mobile.google-play-link' : 'mobile.app-store-link') | translate + }} + + + + + {{ (entityForm.get('platformType').value === PlatformType.ANDROID ? 'mobile.google-play-link-required' : 'mobile.app-store-link-required') | translate }} + + + + mobile.sha256-certificate-fingerprints + + + + + {{ 'mobile.sha256-certificate-fingerprints-required' | translate }} + + + + mobile.app-id + + + + + {{ 'mobile.app-id-required' | translate }} + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.scss new file mode 100644 index 0000000000..0fc2bde23d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.scss @@ -0,0 +1,18 @@ +/** + * 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. + */ +:host { + --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts new file mode 100644 index 0000000000..e801a8966e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts @@ -0,0 +1,153 @@ +/// +/// 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. +/// + +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { AppState } from '@core/core.state'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { Store } from '@ngrx/store'; +import { FormBuilder, FormGroup, UntypedFormControl, Validators } from '@angular/forms'; +import { randomAlphanumeric } from '@core/utils'; +import { EntityType } from '@shared/models/entity-type.models'; +import { MobileApp, MobileAppStatus, mobileAppStatusTranslations } from '@shared/models/mobile-app.models'; +import { PlatformType, platformTypeTranslations } from '@shared/models/oauth2.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +@Component({ + selector: 'tb-mobile-app', + templateUrl: './mobile-app.component.html', + styleUrls: ['./mobile-app.component.scss'] +}) +export class MobileAppComponent extends EntityComponent { + + entityType = EntityType; + + platformTypes = [PlatformType.ANDROID, PlatformType.IOS]; + + MobileAppStatus = MobileAppStatus; + PlatformType = PlatformType; + + mobileAppStatuses = Object.keys(MobileAppStatus) as MobileAppStatus[]; + + platformTypeTranslations = platformTypeTranslations; + mobileAppStatusTranslations = mobileAppStatusTranslations; + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: MobileApp, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected cd: ChangeDetectorRef, + public fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + } + + buildForm(entity: MobileApp): FormGroup { + const form = this.fb.group({ + pkgName: [entity?.pkgName ? entity.pkgName : '', [Validators.required, Validators.maxLength(255), + Validators.pattern(/^\S+$/)]], + platformType: [entity?.platformType ? entity.platformType : PlatformType.ANDROID], + appSecret: [entity?.appSecret ? entity.appSecret : btoa(randomAlphanumeric(64)), [Validators.required, this.base64Format]], + status: [entity?.status ? entity.status : MobileAppStatus.DRAFT], + versionInfo: this.fb.group({ + minVersion: [entity?.versionInfo?.minVersion ? entity.versionInfo.minVersion : ''], + latestVersion: [entity?.versionInfo?.latestVersion ? entity.versionInfo.latestVersion : ''], + }), + storeInfo: this.fb.group({ + storeLink: [entity?.storeInfo?.storeLink ? entity.storeInfo.storeLink : ''], + sha256CertFingerprints: [entity?.storeInfo?.sha256CertFingerprints ? entity.storeInfo.sha256CertFingerprints : ''], + appId: [entity?.storeInfo?.appId ? entity.storeInfo.appId : ''], + }), + }); + + form.get('platformType').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value: PlatformType) => { + if (value === PlatformType.ANDROID) { + form.get('storeInfo.sha256CertFingerprints').enable({emitEvent: false}); + form.get('storeInfo.appId').disable({emitEvent: false}); + } else if (value === PlatformType.IOS) { + form.get('storeInfo.sha256CertFingerprints').disable({emitEvent: false}); + form.get('storeInfo.appId').enable({emitEvent: false}); + } + form.get('storeInfo.storeLink').setValue('', {emitEvent: false}); + }); + + form.get('status').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value: MobileAppStatus) => { + if (value === MobileAppStatus.PUBLISHED) { + form.get('storeInfo.storeLink').addValidators(Validators.required); + form.get('storeInfo.sha256CertFingerprints').addValidators(Validators.required); + form.get('storeInfo.appId').addValidators(Validators.required); + } else { + form.get('storeInfo.storeLink').clearValidators(); + form.get('storeInfo.sha256CertFingerprints').clearValidators(); + form.get('storeInfo.appId').clearValidators(); + } + form.get('storeInfo.storeLink').updateValueAndValidity({emitEvent: false}); + form.get('storeInfo.sha256CertFingerprints').updateValueAndValidity({emitEvent: false}); + form.get('storeInfo.appId').updateValueAndValidity({emitEvent: false}); + }); + + return form; + } + + updateForm(entity: MobileApp) { + this.entityForm.patchValue(entity, {emitEvent: false}); + } + + override updateFormState(): void { + super.updateFormState(); + if (this.isEdit && this.entityForm && !this.isAdd) { + this.entityForm.get('platformType').disable({emitEvent: false}); + if (this.entityForm.get('platformType').value === PlatformType.ANDROID) { + this.entityForm.get('storeInfo.appId').disable({emitEvent: false}); + } else if (this.entityForm.get('platformType').value === PlatformType.IOS) { + this.entityForm.get('storeInfo.sha256CertFingerprints').disable({emitEvent: false}); + } + } + if (this.entityForm && this.isAdd) { + this.entityForm.get('storeInfo.appId').disable({emitEvent: false}); + } + } + + override prepareFormValue(value: MobileApp): MobileApp { + value.storeInfo = this.entityForm.get('storeInfo').value; + return super.prepareFormValue(value); + } + + generateAppSecret($event: Event) { + $event.stopPropagation(); + this.entityForm.get('appSecret').setValue(btoa(randomAlphanumeric(64))); + this.entityForm.get('appSecret').markAsDirty(); + } + + private base64Format(control: UntypedFormControl): { [key: string]: boolean } | null { + if (control.value === '') { + return null; + } + try { + const value = atob(control.value); + if (value.length < 64) { + return {minLength: true}; + } + return null; + } catch (e) { + return {base64: true}; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts new file mode 100644 index 0000000000..4240bb7b78 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts @@ -0,0 +1,78 @@ +/// +/// 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. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { RouterTabsComponent } from '@home/components/router-tabs.component'; +import { Authority } from '@shared/models/authority.enum'; +import { MenuId } from '@core/services/menu.models'; +import { MobileAppTableConfigResolver } from '@home/pages/mobile/applications/mobile-app-table-config.resolver'; +import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { applicationsRoutes } from '@home/pages/mobile/applications/applications-routing.module'; + +const routes: Routes = [ + { + path: 'mobile-center', + component: RouterTabsComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + breadcrumb: { + menuId: MenuId.mobile_center + } + }, + children: [ + { + path: '', + children: [], + data: { + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER, Authority.SYS_ADMIN], + redirectTo: '/mobile-center/applications' + } + }, + ...applicationsRoutes, + { + path: 'mobile-app', + component: MobileAppSettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN], + title: 'admin.mobile-app.mobile-app', + breadcrumb: { + menuId: MenuId.mobile_app_settings + } + } + } + ] + } +]; + +routes.push( + { + path: 'security-settings/oauth2/mobile-applications', + pathMatch: 'full', + redirectTo: '/mobile-center/applications' + } +); + +@NgModule({ + providers: [ + MobileAppTableConfigResolver + ], + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class MobileRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts new file mode 100644 index 0000000000..477fe4ae18 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts @@ -0,0 +1,33 @@ +/// +/// 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. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeComponentsModule } from '@home/components/home-components.module'; +import { MobileRoutingModule } from '@home/pages/mobile/mobile-routing.module'; +import { ApplicationModule } from '@home/pages/mobile/applications/applications.module'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + ApplicationModule, + MobileRoutingModule, + ] +}) +export class MobileModule { } diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 51fefefdf2..88a2ff67bf 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -47,6 +47,7 @@ export enum EntityType { NOTIFICATION_TEMPLATE = 'NOTIFICATION_TEMPLATE', OAUTH2_CLIENT = 'OAUTH2_CLIENT', DOMAIN = 'DOMAIN', + MOBILE_APP_BUNDLE = 'MOBILE_APP_BUNDLE', MOBILE_APP = 'MOBILE_APP' } @@ -458,9 +459,9 @@ export const entityTypeTranslations = new Map([ [BadgePosition.RIGHT, 'admin.mobile-app.right'], [BadgePosition.LEFT, 'admin.mobile-app.left'] ]); + +export type QrCodeConfig = AndroidConfig & IosConfig; + +export enum MobileAppStatus { + DRAFT = 'DRAFT', + PUBLISHED = 'PUBLISHED', + DEPRECATED = 'DEPRECATED', + SUSPENDED = 'SUSPENDED' +} + +export const mobileAppStatusTranslations = new Map( + [ + [MobileAppStatus.DRAFT, 'mobile.status-type.draft'], + [MobileAppStatus.PUBLISHED, 'mobile.status-type.published'], + [MobileAppStatus.DEPRECATED, 'mobile.status-type.deprecated'], + [MobileAppStatus.SUSPENDED, 'mobile.status-type.suspended'], + ] +); + +export interface VersionInfo { + minVersion: string; + minVersionReleaseNotes?: string; + latestVersion: string; + latestVersionReleaseNotes?: string; +} + +export interface StoreInfo { + sha256CertFingerprints?: string; + storeLink: string; + appId?: string; +} + +export interface MobileApp extends BaseData, HasTenantId { + pkgName: string; + appSecret: string; + platformType: PlatformType; + status: MobileAppStatus; + versionInfo: VersionInfo; + storeInfo: StoreInfo; +} + +enum MobileMenuPath { + HOME = 'HOME', + ASSETS = 'ASSETS', + DEVICES = 'DEVICES', + DEVICE_LIST = 'DEVICE_LIST', + ALARMS = 'ALARMS', + DASHBOARDS = 'DASHBOARDS', + DASHBOARD = 'DASHBOARD', + AUDIT_LOGS = 'AUDIT_LOGS', + CUSTOMERS = 'CUSTOMERS', + CUSTOMER = 'CUSTOMER', + NOTIFICATION = 'NOTIFICATION', + CUSTOM = 'CUSTOM' +} + +export interface MobileMenuItem { + label: string; + icon: string; + path: MobileMenuPath; + id: string; +} + +export interface MobileLayoutConfig { + items: MobileMenuItem[]; +} + +export interface MobileAppBundle extends Omit, 'label'>, HasTenantId { + title?: string; + description?: string; + androidAppId?: MobileAppId; + iosAppId?: MobileAppId; + layoutConfig?: MobileLayoutConfig; + oauth2Enabled: boolean; +} + +export interface MobileAppBundleInfo extends MobileAppBundle { + androidPkgName: string; + iosPkgName: string; + oauth2ClientInfos?: Array; +} diff --git a/ui-ngx/src/app/shared/models/oauth2.models.ts b/ui-ngx/src/app/shared/models/oauth2.models.ts index dd024715d5..9c7cd6f342 100644 --- a/ui-ngx/src/app/shared/models/oauth2.models.ts +++ b/ui-ngx/src/app/shared/models/oauth2.models.ts @@ -20,7 +20,6 @@ import { TenantId } from '@shared/models/id/tenant-id'; import { HasTenantId } from './entity.models'; import { DomainId } from './id/domain-id'; import { HasUUID } from '@shared/models/id/has-uuid'; -import { MobileAppId } from '@shared/models/id/mobile-app-id'; export enum DomainSchema { HTTP = 'HTTP', @@ -88,17 +87,6 @@ export interface DomainInfo extends Domain, HasOauth2Clients { oauth2ClientInfos?: Array | Array; } -export interface MobileApp extends BaseData, HasTenantId { - tenantId?: TenantId; - pkgName: string; - appSecret: string; - oauth2Enabled: boolean; -} - -export interface MobileAppInfo extends MobileApp, HasOauth2Clients { - oauth2ClientInfos?: Array | Array; -} - export interface OAuth2Client extends BaseData, HasTenantId { tenantId?: TenantId; title: string; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d4a8cffa53..61305d3b77 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -322,6 +322,7 @@ "mobile-package-placeholder": "Ex.: my.example.app", "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.", "mobile-package-unique": "Application package must be unique.", + "mobile-package-required": "Application package is required.", "mobile-package-max-length": "Application package should be less than 256", "mobile-package-spaces": "Application package should not contain spaces", "mobile-app-secret": "Application secret", @@ -4057,6 +4058,50 @@ "copy-code": "Click to copy", "copied": "Copied!" }, + "mobile": { + "add-application": "Add application", + "app-id": "App ID", + "app-id-required": "App ID is required", + "app-store-link": "App Store link", + "app-store-link-required": "App Store link is required", + "application-details": "Application details", + "application-package": "Application Package", + "application-secret": "Application Secret", + "applications": "Applications", + "copy-app-id": "Copy App ID", + "copy-app-store-link": "Copy App Store link", + "copy-application-package": "Copy application package", + "copy-application-secret": "Copy application secret", + "copy-google-play-link": "Copy Google Play link", + "copy-sha256-certificate-fingerprints": "Copy SHA256 certificate fingerprints", + "delete-applications-text": "Be careful, after the confirmation the mobile application and all related data will become unrecoverable.", + "delete-applications-title": "Are you sure you want to delete the mobile application '{{applicationName}}'?", + "generate-application-secret": "Generate application secret", + "google-play-link": "Google Play link", + "google-play-link-required": "Google Play link is required", + "latest-version": "Latest version", + "min-version": "Min version", + "mobile-center": "Mobile center", + "mobile-package": "Application package", + "mobile-package-max-length": "Application package should be less than 256", + "mobile-package-required": "Application package is required.", + "mobile-package-spaces": "Application package should not contain spaces", + "no=application": "No applications found", + "platform-type": "Platform type", + "search-application": "Search applications", + "set": "Set", + "sha256-certificate-fingerprints": "SHA256 certificate fingerprints", + "sha256-certificate-fingerprints-required": "SHA256 certificate fingerprints is required", + "status": "Status", + "status-type": { + "deprecated": "Deprecated", + "draft": "Draft", + "published": "Published", + "suspended": "Suspended" + }, + "store-information": "Store information", + "version-information": "Version information" + }, "notification": { "action-button": "Action button", "action-type": "Action type", From 119354a44181ab0d3fba3c8c918462e5e9aae7a9 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 11 Oct 2024 11:59:32 +0300 Subject: [PATCH 10/48] renamed db column, added validation on mobile app creation --- .../main/data/upgrade/3.8.0/schema_update.sql | 10 ++-- .../dao/mobile/MobileAppServiceImpl.java | 4 ++ .../server/dao/model/ModelConstants.java | 2 +- .../server/dao/model/sql/MobileAppEntity.java | 8 ++-- .../validator/MobileAppDataValidator.java | 46 +++++++++++++++++++ .../QrCodeSettingsDataValidator.java | 21 +++++++++ .../main/resources/sql/schema-entities.sql | 2 +- 7 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppDataValidator.java diff --git a/application/src/main/data/upgrade/3.8.0/schema_update.sql b/application/src/main/data/upgrade/3.8.0/schema_update.sql index 198d43b5b8..c08db9a5d8 100644 --- a/application/src/main/data/upgrade/3.8.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.0/schema_update.sql @@ -33,7 +33,7 @@ CREATE TABLE IF NOT EXISTS mobile_app_bundle ( ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS platform_type varchar(32), ADD COLUMN IF NOT EXISTS status varchar(32), ADD COLUMN IF NOT EXISTS version_info varchar(16384), - ADD COLUMN IF NOT EXISTS qr_code_config varchar(16384), + ADD COLUMN IF NOT EXISTS store_info varchar(16384), DROP CONSTRAINT IF EXISTS mobile_app_pkg_name_key; -- rename mobile_app_oauth2_client to mobile_app_bundle_oauth2_client @@ -107,7 +107,7 @@ $$ SELECT id into androidAppId FROM mobile_app WHERE pkg_name = qrCodeRecord.android_config::jsonb ->> 'appPackage' AND platform_type = 'ANDROID'; IF androidAppId IS NULL THEN androidAppId := uuid_generate_v4(); - INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, qr_code_config) + INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, store_info) VALUES (androidAppId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, qrCodeRecord.android_config::jsonb ->> 'appPackage', 'ANDROID', 'PUBLISHED', qrCodeRecord.android_config::jsonb - 'appPackage'); generatedBundleId := uuid_generate_v4(); @@ -115,7 +115,7 @@ $$ VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, 'App bundle for qr code', androidAppId); UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId WHERE id = qrCodeRecord.id; ELSE - UPDATE mobile_app SET qr_code_config = qrCodeRecord.android_config::jsonb - 'appPackage' WHERE id = androidAppId; + UPDATE mobile_app SET store_info = qrCodeRecord.android_config::jsonb - 'appPackage' WHERE id = androidAppId; UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.android_app_id = androidAppId) WHERE id = qrCodeRecord.id; END IF; @@ -124,7 +124,7 @@ $$ SELECT id into iosAppId FROM mobile_app WHERE pkg_name = iosPkgName AND platform_type = 'IOS'; IF iosAppId IS NULL THEN iosAppId := uuid_generate_v4(); - INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, qr_code_config) + INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, store_info) VALUES (iosAppId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, iosPkgName, 'IOS', 'PUBLISHED', qrCodeRecord.ios_config); IF generatedBundleId IS NULL THEN @@ -136,7 +136,7 @@ $$ UPDATE mobile_app_bundle SET ios_app_id = iosAppId WHERE id = generatedBundleId; END IF; ELSE - UPDATE mobile_app SET qr_code_config = qrCodeRecord.ios_config WHERE id = iosAppId; + UPDATE mobile_app SET store_info = qrCodeRecord.ios_config WHERE id = iosAppId; UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.ios_app_id = iosAppId) WHERE id = qrCodeRecord.id; END IF; END LOOP; diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java index 8e043c92eb..f1f93bb2d2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java @@ -32,6 +32,7 @@ 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.service.DataValidator; import java.util.Map; import java.util.Optional; @@ -42,10 +43,13 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil @Autowired private MobileAppDao mobileAppDao; + @Autowired + private DataValidator mobileAppDataValidator; @Override public MobileApp saveMobileApp(TenantId tenantId, MobileApp mobileApp) { log.trace("Executing saveMobileApp [{}]", mobileApp); + mobileAppDataValidator.validate(mobileApp, a -> tenantId); try { MobileApp savedMobileApp = mobileAppDao.save(tenantId, mobileApp); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(savedMobileApp).build()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index cdf6dc9b60..87ce78181f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -451,7 +451,7 @@ public class ModelConstants { 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_STORE_INFO_PROPERTY = "store_info"; /** * Mobile application bundle constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java index 4a8798ec89..e9b0b4be9e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java @@ -67,8 +67,8 @@ public class MobileAppEntity extends BaseSqlEntity { private JsonNode versionInfo; @Convert(converter = JsonConverter.class) - @Column(name = ModelConstants.MOBILE_APP_QR_CODE_CONFIG_PROPERTY) - private JsonNode qrCodeConfig; + @Column(name = ModelConstants.MOBILE_APP_STORE_INFO_PROPERTY) + private JsonNode storeInfo; public MobileAppEntity() { super(); @@ -84,7 +84,7 @@ public class MobileAppEntity extends BaseSqlEntity { this.platformType = mobile.getPlatformType(); this.status = mobile.getStatus(); this.versionInfo = toJson(mobile.getVersionInfo()); - this.qrCodeConfig = toJson(mobile.getStoreInfo()); + this.storeInfo = toJson(mobile.getStoreInfo()); } @Override @@ -100,7 +100,7 @@ public class MobileAppEntity extends BaseSqlEntity { mobile.setPlatformType(platformType); mobile.setStatus(status); mobile.setVersionInfo(fromJson(versionInfo, MobileAppVersionInfo.class)); - mobile.setStoreInfo(fromJson(qrCodeConfig, StoreInfo.class)); + mobile.setStoreInfo(fromJson(storeInfo, StoreInfo.class)); return mobile; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppDataValidator.java new file mode 100644 index 0000000000..dbeee48def --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppDataValidator.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.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.MobileApp; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; + +@Component +@AllArgsConstructor +public class MobileAppDataValidator extends DataValidator { + + @Override + protected void validateDataImpl(TenantId tenantId, MobileApp mobileApp) { + if (mobileApp.getPlatformType() == PlatformType.ANDROID) { + if (mobileApp.getStoreInfo() != null && mobileApp.getStoreInfo().isEnabled() && + (mobileApp.getStoreInfo().getSha256CertFingerprints() == null || mobileApp.getStoreInfo().getStoreLink() == null)) { + throw new DataValidationException("Sha256CertFingerprints and store link are required"); + } + } else if (mobileApp.getPlatformType() == PlatformType.IOS) { + if (mobileApp.getStoreInfo() != null && mobileApp.getStoreInfo().isEnabled() && + (mobileApp.getStoreInfo().getAppId() == null || mobileApp.getStoreInfo().getStoreLink() == null)) { + throw new DataValidationException("AppId and store link are required"); + } + } else { + throw new DataValidationException("Wrong application platform type"); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java index 073b5eb53e..c6531c6f85 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java @@ -16,18 +16,26 @@ package org.thingsboard.server.dao.service.validator; import lombok.AllArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileApp; import org.thingsboard.server.common.data.mobile.QRCodeConfig; import org.thingsboard.server.common.data.mobile.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.StoreInfo; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.mobile.MobileAppDao; import org.thingsboard.server.dao.service.DataValidator; @Component @AllArgsConstructor public class QrCodeSettingsDataValidator extends DataValidator { + @Autowired + MobileAppDao mobileAppDao; + @Override protected void validateDataImpl(TenantId tenantId, QrCodeSettings qrCodeSettings) { MobileAppBundleId mobileAppBundleId = qrCodeSettings.getMobileAppBundleId(); @@ -35,6 +43,19 @@ public class QrCodeSettingsDataValidator extends DataValidator { if (!qrCodeSettings.isUseDefaultApp() && (mobileAppBundleId == null)) { throw new DataValidationException("Mobile app bundle is required to use custom application!"); } + if (!qrCodeSettings.isUseDefaultApp()){ + MobileApp androidApp = mobileAppDao.findByBundleIdAndPlatformType(tenantId, mobileAppBundleId, PlatformType.ANDROID); + StoreInfo androidStoreInfo = androidApp.getStoreInfo(); + if (androidStoreInfo == null) { + throw new DataValidationException("Android app store info is empty! "); + } + + MobileApp iosApp = mobileAppDao.findByBundleIdAndPlatformType(tenantId, mobileAppBundleId, PlatformType.IOS); + StoreInfo iosStoreInfo = iosApp.getStoreInfo(); + if (iosStoreInfo == null) { + throw new DataValidationException("IOS app store info is empty! "); + } + } if (qrCodeConfig == null) { throw new DataValidationException("Qr code configuration is required!"); } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 972726a24c..51fab17d3f 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -637,7 +637,7 @@ CREATE TABLE IF NOT EXISTS mobile_app ( platform_type varchar(32), status varchar(32), version_info varchar(16384), - qr_code_config varchar(16384), + store_info varchar(16384), CONSTRAINT pkg_platform_unique UNIQUE (pkg_name, platform_type) ); From ed24383e2a8cc8fc64c87256ec1ca2d097609302 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 16 Oct 2024 10:54:26 +0300 Subject: [PATCH 11/48] UI: Add release note to mobile application --- .../applications/applications.module.ts | 4 +- .../mobile-app-table-config.resolver.ts | 4 +- .../applications/mobile-app.component.html | 41 ++++++++-- .../applications/mobile-app.component.ts | 49 ++++++++++- .../release-notes-panel.component.html | 36 ++++++++ .../release-notes-panel.component.scss | 41 ++++++++++ .../release-notes-panel.component.ts | 82 +++++++++++++++++++ .../assets/locale/locale.constant-en_US.json | 5 +- 8 files changed, 247 insertions(+), 15 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.ts diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts index c213ab9ee6..a3a05d6a50 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts @@ -21,11 +21,13 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { HomeComponentsModule } from '@home/components/home-components.module'; import { ApplicationsRoutingModule } from '@home/pages/mobile/applications/applications-routing.module'; +import { ReleaseNotesPanelComponent } from '@home/pages/mobile/applications/release-notes-panel.component'; @NgModule({ declarations: [ MobileAppComponent, - MobileAppTableHeaderComponent + MobileAppTableHeaderComponent, + ReleaseNotesPanelComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts index 2c86f6124c..ef513728a9 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts @@ -120,7 +120,7 @@ export class MobileAppTableConfigResolver { backgroundColor = 'rgba(209, 39, 48, 0.06)'; break; case MobileAppStatus.DRAFT: - backgroundColor = 'rgba(160, 160, 160, 0.06)'; + backgroundColor = 'rgba(0, 148, 255, 0.06)'; break; } return `
+ {{ 'mobile.application-secret-required' | translate }} + mobile.status @@ -80,14 +83,36 @@
mobile.version-information
- - mobile.min-version - - - - mobile.latest-version - - +
+ + mobile.min-version + + + +
+
+ + mobile.latest-version + + + +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts index e801a8966e..c4eb358a22 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Inject } from '@angular/core'; +import { ChangeDetectorRef, Component, Inject, Renderer2, ViewContainerRef } from '@angular/core'; import { EntityComponent } from '@home/components/entity/entity.component'; import { AppState } from '@core/core.state'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; @@ -26,6 +26,9 @@ import { EntityType } from '@shared/models/entity-type.models'; import { MobileApp, MobileAppStatus, mobileAppStatusTranslations } from '@shared/models/mobile-app.models'; import { PlatformType, platformTypeTranslations } from '@shared/models/oauth2.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { ReleaseNotesPanelComponent } from '@home/pages/mobile/applications/release-notes-panel.component'; @Component({ selector: 'tb-mobile-app', @@ -51,7 +54,10 @@ export class MobileAppComponent extends EntityComponent { @Inject('entity') protected entityValue: MobileApp, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, protected cd: ChangeDetectorRef, - public fb: FormBuilder) { + public fb: FormBuilder, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef) { super(store, fb, entityValue, entitiesTableConfigValue, cd); } @@ -64,7 +70,9 @@ export class MobileAppComponent extends EntityComponent { status: [entity?.status ? entity.status : MobileAppStatus.DRAFT], versionInfo: this.fb.group({ minVersion: [entity?.versionInfo?.minVersion ? entity.versionInfo.minVersion : ''], + minVersionReleaseNotes: [entity?.versionInfo?.minVersionReleaseNotes ? entity.versionInfo.minVersionReleaseNotes : ''], latestVersion: [entity?.versionInfo?.latestVersion ? entity.versionInfo.latestVersion : ''], + latestVersionReleaseNotes: [entity?.versionInfo?.latestVersionReleaseNotes ? entity.versionInfo.latestVersionReleaseNotes : ''], }), storeInfo: this.fb.group({ storeLink: [entity?.storeInfo?.storeLink ? entity.storeInfo.storeLink : ''], @@ -89,7 +97,7 @@ export class MobileAppComponent extends EntityComponent { form.get('status').valueChanges.pipe( takeUntilDestroyed() ).subscribe((value: MobileAppStatus) => { - if (value === MobileAppStatus.PUBLISHED) { + if (value !== MobileAppStatus.DRAFT) { form.get('storeInfo.storeLink').addValidators(Validators.required); form.get('storeInfo.sha256CertFingerprints').addValidators(Validators.required); form.get('storeInfo.appId').addValidators(Validators.required); @@ -113,6 +121,7 @@ export class MobileAppComponent extends EntityComponent { override updateFormState(): void { super.updateFormState(); if (this.isEdit && this.entityForm && !this.isAdd) { + this.entityForm.get('status').updateValueAndValidity({onlySelf: false}); this.entityForm.get('platformType').disable({emitEvent: false}); if (this.entityForm.get('platformType').value === PlatformType.ANDROID) { this.entityForm.get('storeInfo.appId').disable({emitEvent: false}); @@ -136,6 +145,40 @@ export class MobileAppComponent extends EntityComponent { this.entityForm.get('appSecret').markAsDirty(); } + editReleaseNote($event: Event, matButton: MatButton, isLatest: boolean) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + disabled: !(this.isAdd || this.isEdit), + isLatest: isLatest, + releaseNotes: isLatest + ? this.entityForm.get('versionInfo.latestVersionReleaseNotes').value + : this.entityForm.get('versionInfo.minVersionReleaseNotes').value + }; + const releaseNotesPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, ReleaseNotesPanelComponent, ['leftOnly', 'leftBottomOnly', 'leftTopOnly'], true, null, + ctx, + {}, + {}, {}, false, () => {}, {padding: '16px 24px'}); + releaseNotesPanelPopover.tbComponentRef.instance.popover = releaseNotesPanelPopover; + releaseNotesPanelPopover.tbComponentRef.instance.releaseNotesApplied.subscribe((releaseNotes) => { + releaseNotesPanelPopover.hide(); + if (isLatest) { + this.entityForm.get('versionInfo.latestVersionReleaseNotes').setValue(releaseNotes); + this.entityForm.get('versionInfo.latestVersionReleaseNotes').markAsDirty(); + } else { + this.entityForm.get('versionInfo.minVersionReleaseNotes').setValue(releaseNotes); + this.entityForm.get('versionInfo.minVersionReleaseNotes').markAsDirty(); + } + }); + } + } + private base64Format(control: UntypedFormControl): { [key: string]: boolean } | null { if (control.value === '') { return null; diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.html new file mode 100644 index 0000000000..d667a54b01 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.html @@ -0,0 +1,36 @@ + +
+
{{ title | translate }}
+ +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.scss new file mode 100644 index 0000000000..f70487a708 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.scss @@ -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. + */ +@import '../../../../../../scss/constants'; + +.tb-release-notes-panel { + width: 600px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-release-notes-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-release-notes-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.ts new file mode 100644 index 0000000000..935e949f3d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.ts @@ -0,0 +1,82 @@ +/// +/// 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. +/// + +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { TbPopoverComponent } from '@shared/components/popover.component'; + +@Component({ + selector: 'tb-release-notes-panel', + templateUrl: './release-notes-panel.component.html', + styleUrls: ['./release-notes-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ReleaseNotesPanelComponent implements OnInit { + + @Input() + disabled: boolean; + + @Input() + releaseNotes: string; + + @Input() + isLatest: boolean; + + @Input() + popover: TbPopoverComponent; + + @Output() + releaseNotesApplied = new EventEmitter(); + + title: string; + + releaseNotesControl: FormControl; + + tinyMceOptions: Record = { + base_url: '/assets/tinymce', + suffix: '.min', + plugins: ['lists'], + menubar: 'edit insert view format', + toolbar: ['fontfamily fontsize | bold italic underline strikethrough forecolor backcolor', + 'alignleft aligncenter alignright alignjustify | bullist'], + toolbar_mode: 'sliding', + height: 400, + autofocus: false, + branding: false, + promotion: false + }; + + constructor(private fb: FormBuilder) { + } + + ngOnInit(): void { + this.releaseNotesControl = this.fb.control(this.releaseNotes); + this.title = this.isLatest ? 'mobile.latest-version-release-notes' : 'mobile.min-version-release-notes'; + if (this.disabled) { + this.releaseNotesControl.disable({emitEvent: false}); + } + } + + cancel() { + this.popover?.hide(); + } + + apply() { + if (this.releaseNotesControl.valid) { + this.releaseNotesApplied.emit(this.releaseNotesControl.value); + } + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 61305d3b77..d600843dd6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4067,6 +4067,7 @@ "application-details": "Application details", "application-package": "Application Package", "application-secret": "Application Secret", + "application-secret-required": "Application Secret is required", "applications": "Applications", "copy-app-id": "Copy App ID", "copy-app-store-link": "Copy App Store link", @@ -4100,7 +4101,9 @@ "suspended": "Suspended" }, "store-information": "Store information", - "version-information": "Version information" + "version-information": "Version information", + "min-version-release-notes": "Min version release notes", + "latest-version-release-notes": "Latest version release notes" }, "notification": { "action-button": "Action button", From 4acb051b0e09596516c27cfd917ca91c9e50ce1a Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 17 Oct 2024 16:30:16 +0300 Subject: [PATCH 12/48] updated structure mobile layout config --- .../main/data/upgrade/3.8.0/schema_update.sql | 15 +++--- .../server/controller/BaseController.java | 4 +- .../controller/MobileAppBundleController.java | 4 +- .../controller/MobileAppController.java | 2 +- .../server/controller/MobileV2Controller.java | 6 +-- .../controller/QrCodeSettingsController.java | 6 +-- .../controller/SystemInfoController.java | 4 +- .../DefaultTbMobileAppBundleService.java | 2 +- .../mobile/DefaultTbMobileAppService.java | 6 +-- .../mobile/TbMobileAppBundleService.java | 2 +- .../entitiy/mobile/TbMobileAppService.java | 2 +- .../DefaultSystemDataLoaderService.java | 2 +- .../MobileAppBundleControllerTest.java | 14 ++--- .../controller/MobileAppControllerTest.java | 4 +- .../QrCodeSettingsControllerTest.java | 12 ++--- .../dao/mobile/MobileAppBundleService.java | 4 +- .../server/dao/mobile/MobileAppService.java | 2 +- .../common/data/mobile/LoginMobileInfo.java | 1 + .../common/data/mobile/UserMobileInfo.java | 1 + .../data/mobile/{ => app}/MobileApp.java | 3 +- .../mobile/{ => app}/MobileAppStatus.java | 2 +- .../{ => app}/MobileAppVersionInfo.java | 2 +- .../data/mobile/{ => app}/StoreInfo.java | 2 +- .../mobile/{ => bundle}/MobileAppBundle.java | 5 +- .../{ => bundle}/MobileAppBundleInfo.java | 10 ++-- .../MobileAppBundleOauth2Client.java | 2 +- .../mobile/layout/AbstractMobilePage.java | 30 +++++++++++ .../CustomMobilePage.java} | 20 ++++--- .../data/mobile/layout/DashdoardPage.java | 39 ++++++++++++++ .../data/mobile/layout/DefaultMobilePage.java | 40 ++++++++++++++ .../DefaultPageId.java} | 17 +++--- .../{ => layout}/MobileLayoutConfig.java | 6 +-- .../common/data/mobile/layout/MobilePage.java | 54 +++++++++++++++++++ .../data/mobile/layout/MobilePageType.java | 24 +++++++++ .../data/mobile/layout/WebViewPage.java | 39 ++++++++++++++ .../{ => qrCodeSettings}/BadgePosition.java | 2 +- .../{ => qrCodeSettings}/QRCodeConfig.java | 3 +- .../{ => qrCodeSettings}/QrCodeSettings.java | 3 +- .../server/dao/mobile/MobileAppBundleDao.java | 6 +-- .../mobile/MobileAppBundleServiceImpl.java | 6 +-- .../server/dao/mobile/MobileAppDao.java | 2 +- .../dao/mobile/MobileAppServiceImpl.java | 2 +- .../dao/mobile/QrCodeSettingService.java | 4 +- .../dao/mobile/QrCodeSettingServiceImpl.java | 8 +-- .../mobile/QrCodeSettingsCaffeineCache.java | 2 +- .../server/dao/mobile/QrCodeSettingsDao.java | 2 +- .../dao/mobile/QrCodeSettingsRedisCache.java | 2 +- .../sql/AbstractMobileAppBundleEntity.java | 4 +- .../dao/model/sql/MobileAppBundleEntity.java | 2 +- .../model/sql/MobileAppBundleInfoEntity.java | 8 +-- .../MobileAppBundleOauth2ClientEntity.java | 2 +- .../server/dao/model/sql/MobileAppEntity.java | 8 +-- .../dao/model/sql/QrCodeSettingsEntity.java | 4 +- .../MobileAppBundleDataValidator.java | 4 +- .../validator/MobileAppDataValidator.java | 6 ++- .../QrCodeSettingsDataValidator.java | 8 +-- .../dao/sql/mobile/JpaMobileAppBundleDao.java | 6 +-- .../dao/sql/mobile/JpaMobileAppDao.java | 2 +- .../dao/sql/mobile/JpaQrCodeSettingsDao.java | 2 +- .../sql/mobile/MobileAppBundleRepository.java | 4 +- .../main/resources/sql/schema-entities.sql | 2 +- .../dao/service/MobileAppServiceTest.java | 2 +- .../thingsboard/rest/client/RestClient.java | 4 +- .../rule/engine/util/TenantIdLoaderTest.java | 2 +- 64 files changed, 366 insertions(+), 134 deletions(-) rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{ => app}/MobileApp.java (98%) rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{ => app}/MobileAppStatus.java (92%) rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{ => app}/MobileAppVersionInfo.java (96%) rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{ => app}/StoreInfo.java (94%) rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{ => bundle}/MobileAppBundle.java (95%) rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{ => bundle}/MobileAppBundleInfo.java (81%) rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{ => bundle}/MobileAppBundleOauth2Client.java (94%) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{MobileMenuItem.java => layout/CustomMobilePage.java} (57%) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashdoardPage.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{MobileMenuPath.java => layout/DefaultPageId.java} (80%) rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{ => layout}/MobileLayoutConfig.java (82%) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePageType.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{ => qrCodeSettings}/BadgePosition.java (91%) rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{ => qrCodeSettings}/QRCodeConfig.java (88%) rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/{ => qrCodeSettings}/QrCodeSettings.java (95%) diff --git a/application/src/main/data/upgrade/3.8.0/schema_update.sql b/application/src/main/data/upgrade/3.8.0/schema_update.sql index c08db9a5d8..99fa6b35aa 100644 --- a/application/src/main/data/upgrade/3.8.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.0/schema_update.sql @@ -34,14 +34,15 @@ ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS platform_type varchar(32), ADD COLUMN IF NOT EXISTS status varchar(32), ADD COLUMN IF NOT EXISTS version_info varchar(16384), ADD COLUMN IF NOT EXISTS store_info varchar(16384), - DROP CONSTRAINT IF EXISTS mobile_app_pkg_name_key; + DROP CONSTRAINT IF EXISTS mobile_app_pkg_name_key, + DROP CONSTRAINT IF EXISTS mobile_app_unq_key; -- rename mobile_app_oauth2_client to mobile_app_bundle_oauth2_client DO $$ BEGIN -- in case of running the upgrade script a second time - IF EXISTS(SELECT * FROM information_schema.tables WHERE table_name = 'mobile_app_oauth2_client') THEN + IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'mobile_app_oauth2_client') THEN ALTER TABLE mobile_app_oauth2_client RENAME TO mobile_app_bundle_oauth2_client; ALTER TABLE mobile_app_bundle_oauth2_client DROP CONSTRAINT IF EXISTS fk_domain; ALTER TABLE mobile_app_bundle_oauth2_client RENAME COLUMN mobile_app_id TO mobile_app_bundle_id; @@ -61,9 +62,9 @@ $$ mobileAppRecord RECORD; BEGIN -- in case of running the upgrade script a second time - IF EXISTS(SELECT * FROM information_schema.columns WHERE table_name = 'mobile_app' and column_name = 'oauth2_enabled') THEN + IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'mobile_app' and column_name = 'oauth2_enabled') THEN UPDATE mobile_app SET platform_type = 'ANDROID' WHERE platform_type IS NULL; - UPDATE mobile_app SET status = 'PUBLISHED' WHERE mobile_app.status IS NULL; + UPDATE mobile_app SET status = 'DRAFT' WHERE mobile_app.status IS NULL; FOR mobileAppRecord IN SELECT * FROM mobile_app LOOP -- duplicate app for iOS platform type @@ -79,8 +80,8 @@ $$ END LOOP; END IF; ALTER TABLE mobile_app DROP COLUMN IF EXISTS oauth2_enabled; - IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'pkg_platform_unique') THEN - ALTER TABLE mobile_app ADD CONSTRAINT pkg_platform_unique UNIQUE (pkg_name, platform_type); + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'pkg_name_platform_unique') THEN + ALTER TABLE mobile_app ADD CONSTRAINT pkg_name_platform_unique UNIQUE (pkg_name, platform_type); END IF; END; $$; @@ -99,7 +100,7 @@ $$ qrCodeRecord RECORD; BEGIN -- in case of running the upgrade script a second time - IF EXISTS(SELECT * FROM information_schema.columns WHERE table_name = 'qr_code_settings' and column_name = 'android_config') THEN + IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'qr_code_settings' and column_name = 'android_config') THEN FOR qrCodeRecord IN SELECT * FROM qr_code_settings LOOP generatedBundleId := NULL; diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 5999be7428..26f20b6346 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -102,8 +102,8 @@ import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetsBundleId; -import org.thingsboard.server.common.data.mobile.MobileApp; -import org.thingsboard.server.common.data.mobile.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java index 96ddc95dcf..aa39dd7fda 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java @@ -34,8 +34,8 @@ 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.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.config.annotations.ApiOperation; diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java index 1e3d8e31f9..17b777ee81 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java @@ -30,7 +30,7 @@ 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.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java index 764c18bfd7..f0a265040e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java @@ -26,9 +26,9 @@ 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.LoginMobileInfo; -import org.thingsboard.server.common.data.mobile.MobileApp; -import org.thingsboard.server.common.data.mobile.MobileAppBundle; -import org.thingsboard.server.common.data.mobile.MobileAppVersionInfo; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.app.MobileAppVersionInfo; import org.thingsboard.server.common.data.mobile.UserMobileInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; import org.thingsboard.server.common.data.oauth2.PlatformType; diff --git a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java index 978102b443..9eaab50ca9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java @@ -33,9 +33,9 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.MobileApp; -import org.thingsboard.server.common.data.mobile.QrCodeSettings; -import org.thingsboard.server.common.data.mobile.StoreInfo; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.app.StoreInfo; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.config.annotations.ApiOperation; diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 3b90def778..9443e58a04 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -34,8 +34,8 @@ 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.QrCodeSettings; -import org.thingsboard.server.common.data.mobile.QRCodeConfig; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QRCodeConfig; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.settings.UserSettingsType; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppBundleService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppBundleService.java index a8c20a772b..5f9e673fe1 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppBundleService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppBundleService.java @@ -24,7 +24,7 @@ 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.common.data.mobile.bundle.MobileAppBundle; import org.thingsboard.server.dao.mobile.MobileAppBundleService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java index be0d4640a0..9b7d887c7e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java @@ -16,20 +16,16 @@ package org.thingsboard.server.service.entitiy.mobile; import lombok.AllArgsConstructor; -import org.apache.commons.collections4.CollectionUtils; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.MobileAppId; -import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; -import java.util.List; - @Service @AllArgsConstructor public class DefaultTbMobileAppService extends AbstractTbEntityService implements TbMobileAppService { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppBundleService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppBundleService.java index d523f6a764..1efb73d0cf 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppBundleService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppBundleService.java @@ -17,7 +17,7 @@ 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 org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; import java.util.List; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java index 97255e16cc..732cc8e31f 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java @@ -16,7 +16,7 @@ package org.thingsboard.server.service.entitiy.mobile; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileApp; public interface TbMobileAppService { diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 1e92b787bb..fd8a59c392 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -69,7 +69,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; -import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java index 1cc0862fa8..9e7204a355 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java @@ -22,9 +22,10 @@ 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.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppStatus; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.PlatformType; @@ -117,14 +118,14 @@ public class MobileAppBundleControllerTest extends AbstractControllerTest { doPut("/api/mobile/bundle/" + savedAppBundle.getId() + "/oauth2Clients", List.of(savedOAuth2Client.getId().getId(), savedOAuth2Client2.getId().getId())); MobileAppBundleInfo retrievedMobileAppBundleInfo = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedAppBundle.getId().getId()); - assertThat(retrievedMobileAppBundleInfo).isEqualTo(new MobileAppBundleInfo(savedAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), + assertThat(retrievedMobileAppBundleInfo).isEqualTo(new MobileAppBundleInfo(savedAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), false, Stream.of(new OAuth2ClientInfo(savedOAuth2Client), new OAuth2ClientInfo(savedOAuth2Client2)) .sorted(Comparator.comparing(OAuth2ClientInfo::getTitle)).collect(Collectors.toList()) )); doPut("/api/mobile/bundle/" + savedAppBundle.getId() + "/oauth2Clients", List.of(savedOAuth2Client2.getId().getId())); MobileAppBundleInfo retrievedMobileAppInfo2 = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedAppBundle.getId().getId()); - assertThat(retrievedMobileAppInfo2).isEqualTo(new MobileAppBundleInfo(savedAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), List.of(new OAuth2ClientInfo(savedOAuth2Client2)))); + assertThat(retrievedMobileAppInfo2).isEqualTo(new MobileAppBundleInfo(savedAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), false, List.of(new OAuth2ClientInfo(savedOAuth2Client2)))); } @Test @@ -140,12 +141,13 @@ public class MobileAppBundleControllerTest extends AbstractControllerTest { MobileAppBundle savedMobileAppBundle = doPost("/api/mobile/bundle?oauth2ClientIds=" + savedOAuth2Client.getId().getId(), mobileAppBundle, MobileAppBundle.class); MobileAppBundleInfo retrievedMobileAppInfo = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedMobileAppBundle.getId().getId()); - assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppBundleInfo(savedMobileAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), List.of(new OAuth2ClientInfo(savedOAuth2Client)))); + assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppBundleInfo(savedMobileAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), false, List.of(new OAuth2ClientInfo(savedOAuth2Client)))); } private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, PlatformType platformType) { MobileApp mobileApp = new MobileApp(); mobileApp.setTenantId(tenantId); + mobileApp.setStatus(MobileAppStatus.DRAFT); mobileApp.setPkgName(mobileAppName); mobileApp.setPlatformType(platformType); mobileApp.setAppSecret(StringUtils.randomAlphanumeric(24)); diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java index b957c15804..fa2feee0ca 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java @@ -21,7 +21,8 @@ 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.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppStatus; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -101,6 +102,7 @@ public class MobileAppControllerTest extends AbstractControllerTest { mobileApp.setPkgName(mobileAppName); mobileApp.setAppSecret(StringUtils.randomAlphanumeric(24)); mobileApp.setPlatformType(platformType); + mobileApp.setStatus(MobileAppStatus.DRAFT); return mobileApp; } diff --git a/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java index 2ea9344f3d..2607c885b6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java @@ -23,12 +23,12 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Value; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.mobile.MobileApp; -import org.thingsboard.server.common.data.mobile.MobileAppBundle; -import org.thingsboard.server.common.data.mobile.MobileAppBundleInfo; -import org.thingsboard.server.common.data.mobile.QRCodeConfig; -import org.thingsboard.server.common.data.mobile.QrCodeSettings; -import org.thingsboard.server.common.data.mobile.StoreInfo; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QRCodeConfig; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.app.StoreInfo; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java index af296f89f2..234d83ec79 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java @@ -18,8 +18,8 @@ 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.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java index 12559080be..2da9ba3c2f 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java @@ -18,7 +18,7 @@ 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.TenantId; -import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/LoginMobileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/LoginMobileInfo.java index 0556e2169d..059dc17295 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/LoginMobileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/LoginMobileInfo.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.mobile; +import org.thingsboard.server.common.data.mobile.app.MobileAppVersionInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; import java.util.List; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java index 671e9ec271..8bd6294f14 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.mobile; import org.thingsboard.server.common.data.HomeDashboardInfo; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig; public record UserMobileInfo(User user, HomeDashboardInfo homeDashboardInfo, MobileLayoutConfig layoutConfig) { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileApp.java similarity index 98% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileApp.java index 2e23547492..16603ab94f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileApp.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.app; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; @@ -51,6 +51,7 @@ public class MobileApp extends BaseData implements HasTenantId, Has @NotNull private PlatformType platformType; @Schema(description = "Application status: PUBLISHED, DEPRECATED, SUSPENDED, DRAFT", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull private MobileAppStatus status; @Schema(description = "Application version info") @Valid diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppStatus.java similarity index 92% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppStatus.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppStatus.java index 3ffe9cbf10..062c3f8b6b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppStatus.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppStatus.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.app; public enum MobileAppStatus { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppVersionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppVersionInfo.java similarity index 96% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppVersionInfo.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppVersionInfo.java index 948955f315..e177aed4a5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppVersionInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppVersionInfo.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.app; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/StoreInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/StoreInfo.java similarity index 94% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/StoreInfo.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/StoreInfo.java index 5ae4023c23..414e60fc35 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/StoreInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/StoreInfo.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.app; import lombok.Builder; import lombok.Data; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java similarity index 95% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundle.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java index 10c8637f35..8f72d1ffa2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundle.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.bundle; import 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; @@ -29,8 +28,8 @@ import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.MobileAppId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig; import org.thingsboard.server.common.data.validation.Length; -import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) @Data diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundleInfo.java similarity index 81% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleInfo.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundleInfo.java index ec076c1007..b88d5decc9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundleInfo.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.bundle; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -34,17 +34,21 @@ public class MobileAppBundleInfo extends MobileAppBundle { private String iosPkgName; @Schema(description = "List of available oauth2 clients") private List oauth2ClientInfos; + @Schema(description = "Indicates if qr code is available for bundle") + private boolean qrCodeEnabled; - public MobileAppBundleInfo(MobileAppBundle mobileApp, String androidPkgName, String iosPkgName) { + public MobileAppBundleInfo(MobileAppBundle mobileApp, String androidPkgName, String iosPkgName, boolean qrCodeEnabled) { super(mobileApp); this.androidPkgName = androidPkgName; this.iosPkgName = iosPkgName; + this.qrCodeEnabled = qrCodeEnabled; } - public MobileAppBundleInfo(MobileAppBundle mobileApp, String androidPkgName, String iosPkgName, List oauth2ClientInfos) { + public MobileAppBundleInfo(MobileAppBundle mobileApp, String androidPkgName, String iosPkgName, boolean qrCodeEnabled, List oauth2ClientInfos) { super(mobileApp); this.androidPkgName = androidPkgName; this.iosPkgName = iosPkgName; + this.qrCodeEnabled = qrCodeEnabled; this.oauth2ClientInfos = oauth2ClientInfos; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleOauth2Client.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundleOauth2Client.java similarity index 94% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleOauth2Client.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundleOauth2Client.java index fcd261ead1..1309adc9f3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppBundleOauth2Client.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundleOauth2Client.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.bundle; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java new file mode 100644 index 0000000000..09ac3a13e4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile.layout; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public abstract class AbstractMobilePage implements MobilePage { + + @Schema(description = "Page label", example = "Air quality", requiredMode = Schema.RequiredMode.REQUIRED) + protected String label; + @Schema(description = "Indicates if page is visible", example = "true", requiredMode = Schema.RequiredMode.REQUIRED) + protected boolean visible; + @Schema(description = "URL of the page icon", example = "home_icon") + protected String icon; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java similarity index 57% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuItem.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java index ef876f88d9..7e1df56fa9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuItem.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.layout; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; @@ -26,16 +26,14 @@ import lombok.NoArgsConstructor; @Builder @NoArgsConstructor @AllArgsConstructor -@EqualsAndHashCode -public class MobileMenuItem { +@EqualsAndHashCode(callSuper = true) +public class CustomMobilePage extends AbstractMobilePage { - @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; + @Schema(description = "Path", example = "") + private String path; + @Override + public MobilePageType getType() { + return MobilePageType.CUSTOM; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashdoardPage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashdoardPage.java new file mode 100644 index 0000000000..ba1d9d8ec6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashdoardPage.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.mobile.layout; + +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(callSuper = true) +public class DashdoardPage extends AbstractMobilePage { + + @Schema(description = "Dashboard id", example = "784f394c-42b6-435a-983c-b7beff2784f9") + private String dashboardId; + + @Override + public MobilePageType getType() { + return MobilePageType.DASHBOARD; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java new file mode 100644 index 0000000000..2c6048565f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.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.layout; + +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(callSuper = true) +public class DefaultMobilePage extends AbstractMobilePage { + + @Schema(description = "Identifier for default page", example = "HOME") + private DefaultPageId id; + + @Override + public MobilePageType getType() { + return MobilePageType.DEFAULT; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuPath.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultPageId.java similarity index 80% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuPath.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultPageId.java index 68d81c146a..20c79688f0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileMenuPath.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultPageId.java @@ -13,20 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.layout; -public enum MobileMenuPath { +public enum DefaultPageId { HOME, - ASSETS, - DEVICES, - DEVICE_LIST, ALARMS, - DASHBOARDS, - DASHBOARD, - AUDIT_LOGS, + DEVICES, CUSTOMERS, - CUSTOMER, - NOTIFICATION, - CUSTOM + ASSETS, + AUDIT_LOGS, + NOTIFICATIONS } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLayoutConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java similarity index 82% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLayoutConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java index 662ceee13f..9480b1177e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileLayoutConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.layout; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; @@ -33,8 +33,8 @@ import java.util.List; @EqualsAndHashCode public class MobileLayoutConfig { - @Schema(description = "List of custom menu items", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "List of pages") @Valid - private List items = new ArrayList<>(); + private List pages = new ArrayList<>(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java new file mode 100644 index 0000000000..4b37ecd87a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java @@ -0,0 +1,54 @@ +/** + * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL + * + * Copyright © 2016-2024 ThingsBoard, Inc. All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of ThingsBoard, Inc. and its suppliers, + * if any. The intellectual and technical concepts contained + * herein are proprietary to ThingsBoard, Inc. + * and its suppliers and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * + * Dissemination of this information or reproduction of this material is strictly forbidden + * unless prior written permission is obtained from COMPANY. + * + * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, + * managers or contractors who have executed Confidentiality and Non-disclosure agreements + * explicitly covering such access. + * + * The copyright notice above does not evidence any actual or intended publication + * or disclosure of this source code, which includes + * information that is confidential and/or proprietary, and is a trade secret, of COMPANY. + * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, + * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT + * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, + * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. + * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION + * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, + * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. + */ +package org.thingsboard.server.common.data.mobile.layout; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import java.io.Serializable; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DefaultMobilePage.class, name = "DEFAULT"), + @JsonSubTypes.Type(value = CustomMobilePage.class, name = "CUSTOM") +}) +public interface MobilePage extends Serializable { + + MobilePageType getType(); + + boolean isVisible(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePageType.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePageType.java new file mode 100644 index 0000000000..830155f241 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePageType.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile.layout; + +public enum MobilePageType { + + DEFAULT, + DASHBOARD, + WEB_VIEW, + CUSTOM +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java new file mode 100644 index 0000000000..bbc3255688 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.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.mobile.layout; + +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(callSuper = true) +public class WebViewPage extends AbstractMobilePage { + + @Schema(description = "Url", example = "/url") + private String url; + + @Override + public MobilePageType getType() { + return MobilePageType.WEB_VIEW; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/BadgePosition.java similarity index 91% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/BadgePosition.java index 89c46a5c12..c0f58b217b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/BadgePosition.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.qrCodeSettings; public enum BadgePosition { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QRCodeConfig.java similarity index 88% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QRCodeConfig.java index 8bc9b23481..893e56a414 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QRCodeConfig.java @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.qrCodeSettings; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.BadgePosition; import org.thingsboard.server.common.data.validation.NoXss; @Data diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QrCodeSettings.java similarity index 95% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeSettings.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QrCodeSettings.java index ed2578cc88..550da0e539 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QrCodeSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QrCodeSettings.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.qrCodeSettings; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; @@ -23,7 +23,6 @@ import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; 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.QrCodeSettingsId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.java index 7b801a3c30..d100536bf5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.java @@ -17,9 +17,9 @@ 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.MobileAppBundleInfo; -import org.thingsboard.server.common.data.mobile.MobileAppBundleOauth2Client; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleOauth2Client; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java index d0735631fe..bbc84e732b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java @@ -25,9 +25,9 @@ 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.MobileAppBundle; -import org.thingsboard.server.common.data.mobile.MobileAppBundleInfo; -import org.thingsboard.server.common.data.mobile.MobileAppBundleOauth2Client; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleOauth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java index 26fd6a3118..b9e8543ca1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java @@ -17,7 +17,7 @@ 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.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java index f1f93bb2d2..9ebda98726 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java @@ -25,7 +25,7 @@ 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.TenantId; -import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java index 0da5115f62..e06f54a2e8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingService.java @@ -16,8 +16,8 @@ package org.thingsboard.server.dao.mobile; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.MobileApp; -import org.thingsboard.server.common.data.mobile.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; import org.thingsboard.server.common.data.oauth2.PlatformType; public interface QrCodeSettingService { diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java index a31c830748..d217c1c463 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java @@ -21,10 +21,10 @@ 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.BadgePosition; -import org.thingsboard.server.common.data.mobile.MobileApp; -import org.thingsboard.server.common.data.mobile.QrCodeSettings; -import org.thingsboard.server.common.data.mobile.QRCodeConfig; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.BadgePosition; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QRCodeConfig; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.service.DataValidator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsCaffeineCache.java index 806d7af0a9..adc66cb988 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsCaffeineCache.java @@ -21,7 +21,7 @@ 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.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("QrCodeSettingsCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsDao.java index 8b3600ceea..5e321bf69c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsDao.java @@ -16,7 +16,7 @@ package org.thingsboard.server.dao.mobile; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; import org.thingsboard.server.dao.Dao; diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsRedisCache.java index 063c79c2e7..f155de6650 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingsRedisCache.java @@ -24,7 +24,7 @@ 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.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("QrCodeSettingsCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractMobileAppBundleEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractMobileAppBundleEntity.java index b340f2864b..47da9bba8f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractMobileAppBundleEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractMobileAppBundleEntity.java @@ -24,8 +24,8 @@ 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.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.util.mapping.JsonConverter; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java index 66b5a98457..be7f7fe842 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java @@ -19,7 +19,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.mobile.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_TABLE_NAME; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleInfoEntity.java index 9248e4f722..dc90f32e38 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleInfoEntity.java @@ -17,7 +17,7 @@ package org.thingsboard.server.dao.model.sql; import lombok.Data; import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.mobile.MobileAppBundleInfo; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; @Data @EqualsAndHashCode(callSuper = true) @@ -25,19 +25,21 @@ public class MobileAppBundleInfoEntity extends AbstractMobileAppBundleEntity { } else { throw new DataValidationException("Wrong application platform type"); } + if (mobileApp.getStatus() == MobileAppStatus.PUBLISHED && mobileApp.getStoreInfo() == null) { + throw new DataValidationException("Store info is required for published apps"); + } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java index c6531c6f85..395f230437 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java @@ -20,10 +20,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.MobileApp; -import org.thingsboard.server.common.data.mobile.QRCodeConfig; -import org.thingsboard.server.common.data.mobile.QrCodeSettings; -import org.thingsboard.server.common.data.mobile.StoreInfo; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QRCodeConfig; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.app.StoreInfo; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.mobile.MobileAppDao; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java index b2b9d913f3..7dcf9786fe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java @@ -21,9 +21,9 @@ 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.MobileAppBundleInfo; -import org.thingsboard.server.common.data.mobile.MobileAppBundleOauth2Client; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleOauth2Client; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java index 5e1670bc4f..f33d67f426 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java @@ -21,7 +21,7 @@ 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.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaQrCodeSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaQrCodeSettingsDao.java index b415af445c..3a478e745c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaQrCodeSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaQrCodeSettingsDao.java @@ -20,7 +20,7 @@ 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.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.mobile.QrCodeSettingsDao; import org.thingsboard.server.dao.model.sql.QrCodeSettingsEntity; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java index 7cc60ddd00..982ff3c9ca 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java @@ -30,7 +30,7 @@ import java.util.UUID; public interface MobileAppBundleRepository extends JpaRepository { - @Query("SELECT new org.thingsboard.server.dao.model.sql.MobileAppBundleInfoEntity(b, andApp.pkgName, iosApp.pkgName) " + + @Query("SELECT new org.thingsboard.server.dao.model.sql.MobileAppBundleInfoEntity(b, andApp.pkgName, iosApp.pkgName, (andApp.status = 'PUBLISHED' or iosApp.status = 'PUBLISHED')) " + "FROM MobileAppBundleEntity b " + "LEFT JOIN MobileAppEntity andApp on b.androidAppId = andApp.id " + "LEFT JOIN MobileAppEntity iosApp on b.iosAppID = iosApp.id " + @@ -40,7 +40,7 @@ public interface MobileAppBundleRepository extends JpaRepository Date: Thu, 17 Oct 2024 17:04:09 +0300 Subject: [PATCH 13/48] added tenant permission for mobile app and bundle --- .../controller/MobileAppBundleController.java | 23 +++++++++---------- .../controller/MobileAppController.java | 19 ++++++++------- .../controller/QrCodeSettingsController.java | 4 ++-- .../permission/CustomerUserPermissions.java | 2 +- .../service/security/permission/Resource.java | 2 +- .../permission/SysAdminPermissions.java | 6 ++--- .../permission/TenantAdminPermissions.java | 4 +++- .../common/data/mobile/layout/MobilePage.java | 2 ++ .../data/mobile/layout/WebViewPage.java | 2 +- 9 files changed, 33 insertions(+), 31 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java index aa39dd7fda..306444a23e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java @@ -51,7 +51,7 @@ import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_ import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; @RestController @@ -68,8 +68,8 @@ public class MobileAppBundleController extends BaseController { "mobile settings like oauth2 clients, self-registration and layout configuration." + "When creating mobile app bundle, platform generates Mobile App Bundle Id as " + UUID_WIKI_LINK + "The newly created Mobile App Bundle Id will be present in the response. " + - "Referencing non-existing Mobile App Bundle Id will cause 'Not Found' error." + SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + "Referencing non-existing Mobile App Bundle Id will cause 'Not Found' error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PostMapping(value = "/mobile/bundle") public MobileAppBundle saveMobileAppBundle( @Parameter(description = "A JSON value representing the Mobile Application Bundle.", required = true) @@ -82,8 +82,8 @@ public class MobileAppBundleController extends BaseController { } @ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)", - notes = "Update oauth2 clients of the specified mobile app bundle. ") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + notes = "Update oauth2 clients of the specified mobile app bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PutMapping(value = "/mobile/bundle/{id}/oauth2Clients") public void updateOauth2Clients(@PathVariable UUID id, @RequestBody UUID[] clientIds) throws ThingsboardException { @@ -93,8 +93,8 @@ public class MobileAppBundleController extends BaseController { tbMobileAppBundleService.updateOauth2Clients(mobileAppBundle, oAuth2ClientIds, getCurrentUser()); } - @ApiOperation(value = "Get mobile app bundle infos (getTenantMobileAppBundleInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @ApiOperation(value = "Get mobile app bundle infos (getTenantMobileAppBundleInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/mobile/bundle/infos") public PageData getTenantMobileAppBundleInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -106,13 +106,12 @@ public class MobileAppBundleController extends BaseController { @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - accessControlService.checkPermission(getCurrentUser(), Resource.MOBILE_APP_BUNDLE, 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')") + @ApiOperation(value = "Get mobile app bundle info by id (getMobileAppBundleInfoById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/mobile/bundle/info/{id}") public MobileAppBundleInfo getMobileAppBundleInfoById(@PathVariable UUID id) throws ThingsboardException { MobileAppBundleId mobileAppBundleId = new MobileAppBundleId(id); @@ -120,8 +119,8 @@ public class MobileAppBundleController extends BaseController { } @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')") + notes = "Deletes Mobile App Bundle by ID. Referencing non-existing mobile app bundle Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @DeleteMapping(value = "/mobile/bundle/{id}") public void deleteMobileAppBundle(@PathVariable UUID id) throws Exception { MobileAppBundleId mobileAppBundleId = new MobileAppBundleId(id); diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java index 17b777ee81..8ccc94502b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java @@ -46,7 +46,7 @@ import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_ import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; @RestController @@ -63,8 +63,8 @@ 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\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')") + "\n\nThe pair of mobile app package name and platform type is unique for entire platform setup.\n\n" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PostMapping(value = "/mobile/app") public MobileApp saveMobileApp( @Parameter(description = "A JSON value representing the Mobile Application.", required = true) @@ -74,8 +74,8 @@ public class MobileAppController extends BaseController { return tbMobileAppService.save(mobileApp, getCurrentUser()); } - @ApiOperation(value = "Get mobile app infos (getTenantMobileAppInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @ApiOperation(value = "Get mobile app infos (getTenantMobileAppInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/mobile/app") public PageData getTenantMobileApps(@Parameter(description = "Platform type: ANDROID or IOS") @RequestParam(required = false) PlatformType platformType, @@ -89,13 +89,12 @@ public class MobileAppController extends BaseController { @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 mobileAppService.findMobileAppsByTenantId(getTenantId(), platformType, pageLink); } - @ApiOperation(value = "Get mobile info by id (getMobileAppInfoById)", notes = SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @ApiOperation(value = "Get mobile info by id (getMobileAppInfoById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/mobile/app/{id}") public MobileApp getMobileAppById(@PathVariable UUID id) throws ThingsboardException { MobileAppId mobileAppId = new MobileAppId(id); @@ -103,8 +102,8 @@ public class MobileAppController extends BaseController { } @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')") + notes = "Deletes Mobile App by ID. Referencing non-existing mobile app Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @DeleteMapping(value = "/mobile/app/{id}") public void deleteMobileApp(@PathVariable UUID id) throws Exception { MobileAppId mobileAppId = new MobileAppId(id); diff --git a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java index 9eaab50ca9..c7bfb19c7b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java @@ -128,7 +128,7 @@ public class QrCodeSettingsController extends BaseController { 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); + accessControlService.checkPermission(currentUser, Resource.QR_CODE_SETTINGS, Operation.WRITE); qrCodeSettings.setTenantId(getTenantId()); return qrCodeSettingService.saveQrCodeSettings(currentUser.getTenantId(), qrCodeSettings); } @@ -139,7 +139,7 @@ public class QrCodeSettingsController extends BaseController { @GetMapping(value = "/api/mobile/qr/settings") public QrCodeSettings getMobileAppSettings() throws ThingsboardException { SecurityUser currentUser = getCurrentUser(); - accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ); + accessControlService.checkPermission(currentUser, Resource.QR_CODE_SETTINGS, Operation.READ); return qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java index 46cd1c2979..fc50d85040 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java @@ -47,7 +47,7 @@ public class CustomerUserPermissions extends AbstractPermissions { put(Resource.DEVICE_PROFILE, profilePermissionChecker); put(Resource.ASSET_PROFILE, profilePermissionChecker); put(Resource.TB_RESOURCE, customerResourcePermissionChecker); - put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ)); + put(Resource.QR_CODE_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ)); } private static final PermissionChecker customerAlarmPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index 8c6f4d00e0..dd169b5a6b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -50,7 +50,7 @@ public enum Resource { VERSION_CONTROL, NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), - MOBILE_APP_SETTINGS; + QR_CODE_SETTINGS; private final Set entityTypes; Resource() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index 8edbd9f6d5..9e4b7d3b1e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java @@ -36,15 +36,15 @@ public class SysAdminPermissions extends AbstractPermissions { put(Resource.WIDGETS_BUNDLE, systemEntityPermissionChecker); 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.MOBILE_APP, systemEntityPermissionChecker); + put(Resource.MOBILE_APP_BUNDLE, systemEntityPermissionChecker); put(Resource.DOMAIN, PermissionChecker.allowAllPermissionChecker); put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, PermissionChecker.allowAllPermissionChecker); put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker); put(Resource.TB_RESOURCE, systemEntityPermissionChecker); put(Resource.QUEUE, systemEntityPermissionChecker); put(Resource.NOTIFICATION, systemEntityPermissionChecker); - put(Resource.MOBILE_APP_SETTINGS, PermissionChecker.allowAllPermissionChecker); + put(Resource.QR_CODE_SETTINGS, PermissionChecker.allowAllPermissionChecker); } private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index 10807e4b5a..dc77480660 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -50,7 +50,9 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.QUEUE, queuePermissionChecker); put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker); put(Resource.NOTIFICATION, tenantEntityPermissionChecker); - put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ)); + put(Resource.QR_CODE_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ)); + put(Resource.MOBILE_APP, tenantEntityPermissionChecker); + put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java index 4b37ecd87a..335974be18 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java @@ -43,6 +43,8 @@ import java.io.Serializable; property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = DefaultMobilePage.class, name = "DEFAULT"), + @JsonSubTypes.Type(value = DashdoardPage.class, name = "DASHBOARD"), + @JsonSubTypes.Type(value = WebViewPage.class, name = "WEB_VIEW"), @JsonSubTypes.Type(value = CustomMobilePage.class, name = "CUSTOM") }) public interface MobilePage extends Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java index bbc3255688..1744034251 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java @@ -27,7 +27,7 @@ import lombok.NoArgsConstructor; @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = true) -public class WebViewPage extends AbstractMobilePage { +public class WebViewPage extends AbstractMobilePage { @Schema(description = "Url", example = "/url") private String url; From fb87cd6df7df1768ecaa688c39ba5b3be6f082e2 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 21 Oct 2024 18:54:22 +0300 Subject: [PATCH 14/48] refactoring --- .../main/data/upgrade/3.8.0/schema_update.sql | 10 +++--- .../server/controller/MobileV2Controller.java | 23 ++++++++++-- .../controller/QrCodeSettingsController.java | 4 +-- .../thingsboard/server/common/data/Views.java | 36 +++++++++++++++++++ .../common/data/mobile/UserMobileInfo.java | 6 ++-- .../mobile/layout/AbstractMobilePage.java | 5 +++ .../data/mobile/layout/CustomMobilePage.java | 3 ++ ...{DashdoardPage.java => DashboardPage.java} | 5 ++- .../data/mobile/layout/DefaultMobilePage.java | 3 ++ .../mobile/layout/MobileLayoutConfig.java | 3 ++ .../common/data/mobile/layout/MobilePage.java | 6 +++- .../data/mobile/layout/WebViewPage.java | 3 ++ .../thingsboard/common/util/JacksonUtil.java | 13 +++++++ 13 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/Views.java rename common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/{DashdoardPage.java => DashboardPage.java} (86%) diff --git a/application/src/main/data/upgrade/3.8.0/schema_update.sql b/application/src/main/data/upgrade/3.8.0/schema_update.sql index 99fa6b35aa..8884919cc5 100644 --- a/application/src/main/data/upgrade/3.8.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.0/schema_update.sql @@ -75,7 +75,7 @@ $$ generatedBundleId := uuid_generate_v4(); INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id, ios_app_id, oauth2_enabled) VALUES (generatedBundleId, (extract(epoch from now()) * 1000), mobileAppRecord.tenant_id, - 'App bundle ' || mobileAppRecord.pkg_name, mobileAppRecord.id, iosAppId, mobileAppRecord.oauth2_enabled); + 'Autogenerated for ' || mobileAppRecord.pkg_name, mobileAppRecord.id, iosAppId, mobileAppRecord.oauth2_enabled); UPDATE mobile_app_bundle_oauth2_client SET mobile_app_bundle_id = generatedBundleId WHERE mobile_app_bundle_id = mobileAppRecord.id; END LOOP; END IF; @@ -110,10 +110,10 @@ $$ androidAppId := uuid_generate_v4(); INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, store_info) VALUES (androidAppId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, - qrCodeRecord.android_config::jsonb ->> 'appPackage', 'ANDROID', 'PUBLISHED', qrCodeRecord.android_config::jsonb - 'appPackage'); + qrCodeRecord.android_config::jsonb ->> 'appPackage', 'ANDROID', 'DRAFT', qrCodeRecord.android_config::jsonb - 'appPackage'); generatedBundleId := uuid_generate_v4(); INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id) - VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, 'App bundle for qr code', androidAppId); + VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, 'Autogenerated for qr code', androidAppId); UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId WHERE id = qrCodeRecord.id; ELSE UPDATE mobile_app SET store_info = qrCodeRecord.android_config::jsonb - 'appPackage' WHERE id = androidAppId; @@ -127,11 +127,11 @@ $$ iosAppId := uuid_generate_v4(); INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, store_info) VALUES (iosAppId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, - iosPkgName, 'IOS', 'PUBLISHED', qrCodeRecord.ios_config); + iosPkgName, 'IOS', 'DRAFT', qrCodeRecord.ios_config); IF generatedBundleId IS NULL THEN generatedBundleId := uuid_generate_v4(); INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, ios_app_id) - VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, 'App bundle for qr code', iosAppId); + VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, 'Autogenerated for qr code', iosAppId); UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId WHERE id = qrCodeRecord.id; ELSE UPDATE mobile_app_bundle SET ios_app_id = iosAppId WHERE id = generatedBundleId; diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java index f0a265040e..15ff2a7346 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; @@ -22,21 +24,26 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.HomeDashboardInfo; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.Views; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.mobile.LoginMobileInfo; import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; import org.thingsboard.server.common.data.mobile.app.MobileAppVersionInfo; import org.thingsboard.server.common.data.mobile.UserMobileInfo; +import org.thingsboard.server.common.data.mobile.layout.MobilePage; import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; @@ -62,12 +69,12 @@ public class MobileV2Controller extends BaseController { public UserMobileInfo getUserMobileInfo(@Parameter(description = "Mobile application package name") @RequestParam String pkgName, @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) - @RequestParam PlatformType platform) throws ThingsboardException { + @RequestParam PlatformType platform) throws ThingsboardException, JsonProcessingException { SecurityUser securityUser = getCurrentUser(); User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId()); HomeDashboardInfo homeDashboardInfo = securityUser.isSystemAdmin() ? null : getHomeDashboardInfo(securityUser, user.getAdditionalInfo()); MobileAppBundle mobileAppBundle = mobileAppBundleService.findMobileAppBundleByPkgNameAndPlatform(securityUser.getTenantId(), pkgName, platform); - return new UserMobileInfo(user, homeDashboardInfo, mobileAppBundle != null ? mobileAppBundle.getLayoutConfig() : null); + return new UserMobileInfo(user, homeDashboardInfo, getVisiblePages(mobileAppBundle)); } @ApiOperation(value = "Get mobile app version info (getMobileVersionInfo)") @@ -80,4 +87,16 @@ public class MobileV2Controller extends BaseController { return mobileApp != null ? mobileApp.getVersionInfo() : null; } + private List getVisiblePages(MobileAppBundle mobileAppBundle) throws JsonProcessingException { + if (mobileAppBundle != null && mobileAppBundle.getLayoutConfig() != null) { + List mobilePages = mobileAppBundle.getLayoutConfig().getPages() + .stream() + .filter(MobilePage::isVisible) + .collect(Collectors.toList()); + return JacksonUtil.readValue(JacksonUtil.writeValueAsViewIgnoringNullFields(mobilePages, Views.Public.class), new TypeReference<>() {}); + } else { + return Collections.emptyList(); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java index c7bfb19c7b..0b2cb8fe8f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java @@ -125,7 +125,7 @@ public class QrCodeSettingsController extends BaseController { 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/qr/settings") - public QrCodeSettings saveMobileAppSettings(@Parameter(description = "A JSON value representing the mobile apps configuration") + public QrCodeSettings saveQrCodeSettings(@Parameter(description = "A JSON value representing the mobile apps configuration") @RequestBody QrCodeSettings qrCodeSettings) throws ThingsboardException { SecurityUser currentUser = getCurrentUser(); accessControlService.checkPermission(currentUser, Resource.QR_CODE_SETTINGS, Operation.WRITE); @@ -137,7 +137,7 @@ public class QrCodeSettingsController extends BaseController { 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/qr/settings") - public QrCodeSettings getMobileAppSettings() throws ThingsboardException { + public QrCodeSettings getQrAppSettings() throws ThingsboardException { SecurityUser currentUser = getCurrentUser(); accessControlService.checkPermission(currentUser, Resource.QR_CODE_SETTINGS, Operation.READ); return qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Views.java b/common/data/src/main/java/org/thingsboard/server/common/data/Views.java new file mode 100644 index 0000000000..498d96fdeb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Views.java @@ -0,0 +1,36 @@ +/** + * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL + * + * Copyright © 2016-2024 ThingsBoard, Inc. All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of ThingsBoard, Inc. and its suppliers, + * if any. The intellectual and technical concepts contained + * herein are proprietary to ThingsBoard, Inc. + * and its suppliers and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * + * Dissemination of this information or reproduction of this material is strictly forbidden + * unless prior written permission is obtained from COMPANY. + * + * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, + * managers or contractors who have executed Confidentiality and Non-disclosure agreements + * explicitly covering such access. + * + * The copyright notice above does not evidence any actual or intended publication + * or disclosure of this source code, which includes + * information that is confidential and/or proprietary, and is a trade secret, of COMPANY. + * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, + * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT + * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, + * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. + * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION + * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, + * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. + */ +package org.thingsboard.server.common.data; + +public class Views { + public static class Public {} + public static class Private extends Public {} +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java index 8bd6294f14..d93a86f7d6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java @@ -17,7 +17,9 @@ package org.thingsboard.server.common.data.mobile; import org.thingsboard.server.common.data.HomeDashboardInfo; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig; +import org.thingsboard.server.common.data.mobile.layout.MobilePage; -public record UserMobileInfo(User user, HomeDashboardInfo homeDashboardInfo, MobileLayoutConfig layoutConfig) { +import java.util.List; + +public record UserMobileInfo(User user, HomeDashboardInfo homeDashboardInfo, List pages) { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java index 09ac3a13e4..4d87530d0e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java @@ -15,16 +15,21 @@ */ package org.thingsboard.server.common.data.mobile.layout; +import com.fasterxml.jackson.annotation.JsonView; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import org.thingsboard.server.common.data.Views; @Data public abstract class AbstractMobilePage implements MobilePage { @Schema(description = "Page label", example = "Air quality", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonView(Views.Public.class) protected String label; @Schema(description = "Indicates if page is visible", example = "true", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonView(Views.Private.class) protected boolean visible; @Schema(description = "URL of the page icon", example = "home_icon") + @JsonView(Views.Public.class) protected String icon; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java index 7e1df56fa9..114e5930d3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.mobile.layout; +import com.fasterxml.jackson.annotation.JsonView; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.Views; @Data @Builder @@ -30,6 +32,7 @@ import lombok.NoArgsConstructor; public class CustomMobilePage extends AbstractMobilePage { @Schema(description = "Path", example = "") + @JsonView(Views.Public.class) private String path; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashdoardPage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java similarity index 86% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashdoardPage.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java index ba1d9d8ec6..8adb50c140 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashdoardPage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java @@ -15,21 +15,24 @@ */ package org.thingsboard.server.common.data.mobile.layout; +import com.fasterxml.jackson.annotation.JsonView; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.Views; @Data @Builder @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = true) -public class DashdoardPage extends AbstractMobilePage { +public class DashboardPage extends AbstractMobilePage { @Schema(description = "Dashboard id", example = "784f394c-42b6-435a-983c-b7beff2784f9") + @JsonView(Views.Public.class) private String dashboardId; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java index 2c6048565f..c4504ee1e2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.mobile.layout; +import com.fasterxml.jackson.annotation.JsonView; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.Views; @Data @Builder @@ -30,6 +32,7 @@ import lombok.NoArgsConstructor; public class DefaultMobilePage extends AbstractMobilePage { @Schema(description = "Identifier for default page", example = "HOME") + @JsonView(Views.Public.class) private DefaultPageId id; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java index 9480b1177e..5add86dbef 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.mobile.layout; +import com.fasterxml.jackson.annotation.JsonView; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import lombok.AllArgsConstructor; @@ -22,6 +23,7 @@ import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.Views; import java.util.ArrayList; import java.util.List; @@ -34,6 +36,7 @@ import java.util.List; public class MobileLayoutConfig { @Schema(description = "List of pages") + @JsonView(Views.Public.class) @Valid private List pages = new ArrayList<>(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java index 335974be18..2491c4e752 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java @@ -33,6 +33,8 @@ package org.thingsboard.server.common.data.mobile.layout; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonView; +import org.thingsboard.server.common.data.Views; import java.io.Serializable; @@ -43,14 +45,16 @@ import java.io.Serializable; property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = DefaultMobilePage.class, name = "DEFAULT"), - @JsonSubTypes.Type(value = DashdoardPage.class, name = "DASHBOARD"), + @JsonSubTypes.Type(value = DashboardPage.class, name = "DASHBOARD"), @JsonSubTypes.Type(value = WebViewPage.class, name = "WEB_VIEW"), @JsonSubTypes.Type(value = CustomMobilePage.class, name = "CUSTOM") }) public interface MobilePage extends Serializable { + @JsonView(Views.Private.class) MobilePageType getType(); + @JsonView(Views.Private.class) boolean isVisible(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java index 1744034251..afb9ca7798 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.mobile.layout; +import com.fasterxml.jackson.annotation.JsonView; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.Views; @Data @Builder @@ -30,6 +32,7 @@ import lombok.NoArgsConstructor; public class WebViewPage extends AbstractMobilePage { @Schema(description = "Url", example = "/url") + @JsonView(Views.Public.class) private String url; @Override diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 02af9b53f8..b979eeeb64 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -35,6 +35,7 @@ import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.Views; import java.io.File; import java.io.IOException; @@ -174,6 +175,10 @@ public class JacksonUtil { } } + public static String writeValueAsViewIgnoringNullFields(Object value, Class serializationView) throws JsonProcessingException { + return value == null ? "" : OBJECT_MAPPER.writerWithView(serializationView).writeValueAsString(value); + } + public static String toPrettyString(Object o) { try { return PRETTY_SORTED_JSON_MAPPER.writeValueAsString(o); @@ -227,6 +232,14 @@ public class JacksonUtil { } } + public static T readValue(String object, TypeReference clazz) { + try { + return OBJECT_MAPPER.readValue(object, clazz); + } catch (IOException e) { + throw new IllegalArgumentException("Can't read object: " + object, e); + } + } + public static T readValue(File file, TypeReference clazz) { try { return OBJECT_MAPPER.readValue(file, clazz); From 879e8f46a5fc893c1392cda53296fc161ec3d1c9 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 22 Oct 2024 12:51:04 +0300 Subject: [PATCH 15/48] pull request comments --- .../controller/MobileAppController.java | 65 +++++++++++ .../server/controller/MobileV2Controller.java | 102 ------------------ .../controller/QrCodeSettingsController.java | 8 +- .../permission/CustomerUserPermissions.java | 2 +- .../service/security/permission/Resource.java | 2 +- .../permission/SysAdminPermissions.java | 2 +- .../permission/TenantAdminPermissions.java | 2 +- .../src/main/resources/thingsboard.yml | 2 +- .../mobile/MobileAppBundleServiceImpl.java | 3 - .../server/dao/tenant/TenantServiceImpl.java | 2 +- .../server/dao/user/UserServiceImpl.java | 8 +- 11 files changed, 79 insertions(+), 119 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java index 8ccc94502b..8f74aab7af 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java @@ -15,7 +15,10 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -28,20 +31,35 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.HomeDashboardInfo; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.Views; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.mobile.LoginMobileInfo; +import org.thingsboard.server.common.data.mobile.UserMobileInfo; import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppVersionInfo; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.layout.MobilePage; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.mobile.TbMobileAppService; +import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.Collections; +import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; +import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; @@ -58,6 +76,41 @@ public class MobileAppController extends BaseController { private final TbMobileAppService tbMobileAppService; + @ApiOperation(value = "Get mobile app login info (getLoginMobileInfo)") + @GetMapping(value = "/api/noauth/mobile") + public LoginMobileInfo getLoginMobileInfo(@Parameter(description = "Mobile application package name") + @RequestParam String pkgName, + @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) + @RequestParam PlatformType platform) { + List oauth2Clients = oAuth2ClientService.findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(pkgName, platform); + MobileApp mobileApp = mobileAppService.findMobileAppByPkgNameAndPlatformType(pkgName, platform); + return new LoginMobileInfo(oauth2Clients, mobileApp != null ? mobileApp.getVersionInfo() : null); + } + + @ApiOperation(value = "Get user mobile app basic info (getUserMobileInfo)", notes = AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/api/mobile") + public UserMobileInfo getUserMobileInfo(@Parameter(description = "Mobile application package name") + @RequestParam String pkgName, + @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) + @RequestParam PlatformType platform) throws ThingsboardException, JsonProcessingException { + SecurityUser securityUser = getCurrentUser(); + User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId()); + HomeDashboardInfo homeDashboardInfo = securityUser.isSystemAdmin() ? null : getHomeDashboardInfo(securityUser, user.getAdditionalInfo()); + MobileAppBundle mobileAppBundle = mobileAppBundleService.findMobileAppBundleByPkgNameAndPlatform(securityUser.getTenantId(), pkgName, platform); + return new UserMobileInfo(user, homeDashboardInfo, getVisiblePages(mobileAppBundle)); + } + + @ApiOperation(value = "Get mobile app version info (getMobileVersionInfo)") + @GetMapping(value = "/api/mobile/versionInfo") + public MobileAppVersionInfo getMobileVersionInfo(@Parameter(description = "Mobile application package name") + @RequestParam String pkgName, + @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) + @RequestParam PlatformType platform) { + MobileApp mobileApp = mobileAppService.findMobileAppByPkgNameAndPlatformType(pkgName, platform); + return mobileApp != null ? mobileApp.getVersionInfo() : null; + } + @ApiOperation(value = "Save Or update Mobile app (saveMobileApp)", notes = "Create or update the Mobile app. When creating mobile app, platform generates Mobile App Id as " + UUID_WIKI_LINK + "The newly created Mobile App Id will be present in the response. " + @@ -111,4 +164,16 @@ public class MobileAppController extends BaseController { tbMobileAppService.delete(mobileApp, getCurrentUser()); } + private List getVisiblePages(MobileAppBundle mobileAppBundle) throws JsonProcessingException { + if (mobileAppBundle != null && mobileAppBundle.getLayoutConfig() != null) { + List mobilePages = mobileAppBundle.getLayoutConfig().getPages() + .stream() + .filter(MobilePage::isVisible) + .collect(Collectors.toList()); + return JacksonUtil.readValue(JacksonUtil.writeValueAsViewIgnoringNullFields(mobilePages, Views.Public.class), new TypeReference<>() {}); + } else { + return Collections.emptyList(); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java deleted file mode 100644 index 15ff2a7346..0000000000 --- a/application/src/main/java/org/thingsboard/server/controller/MobileV2Controller.java +++ /dev/null @@ -1,102 +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.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.RequiredArgsConstructor; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.HomeDashboardInfo; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.Views; -import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.mobile.LoginMobileInfo; -import org.thingsboard.server.common.data.mobile.app.MobileApp; -import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; -import org.thingsboard.server.common.data.mobile.app.MobileAppVersionInfo; -import org.thingsboard.server.common.data.mobile.UserMobileInfo; -import org.thingsboard.server.common.data.mobile.layout.MobilePage; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; -import org.thingsboard.server.common.data.oauth2.PlatformType; -import org.thingsboard.server.config.annotations.ApiOperation; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.security.model.SecurityUser; - -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; - -@RequiredArgsConstructor -@RestController -@TbCoreComponent -public class MobileV2Controller extends BaseController { - - @ApiOperation(value = "Get mobile app login info (getLoginMobileInfo)") - @GetMapping(value = "/api/noauth/mobile") - public LoginMobileInfo getLoginMobileInfo(@Parameter(description = "Mobile application package name") - @RequestParam String pkgName, - @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) - @RequestParam PlatformType platform) { - List oauth2Clients = oAuth2ClientService.findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(pkgName, platform); - MobileApp mobileApp = mobileAppService.findMobileAppByPkgNameAndPlatformType(pkgName, platform); - return new LoginMobileInfo(oauth2Clients, mobileApp != null ? mobileApp.getVersionInfo() : null); - } - - @ApiOperation(value = "Get user mobile app basic info (getUserMobileInfo)", notes = AVAILABLE_FOR_ANY_AUTHORIZED_USER) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/api/mobile") - public UserMobileInfo getUserMobileInfo(@Parameter(description = "Mobile application package name") - @RequestParam String pkgName, - @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) - @RequestParam PlatformType platform) throws ThingsboardException, JsonProcessingException { - SecurityUser securityUser = getCurrentUser(); - User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId()); - HomeDashboardInfo homeDashboardInfo = securityUser.isSystemAdmin() ? null : getHomeDashboardInfo(securityUser, user.getAdditionalInfo()); - MobileAppBundle mobileAppBundle = mobileAppBundleService.findMobileAppBundleByPkgNameAndPlatform(securityUser.getTenantId(), pkgName, platform); - return new UserMobileInfo(user, homeDashboardInfo, getVisiblePages(mobileAppBundle)); - } - - @ApiOperation(value = "Get mobile app version info (getMobileVersionInfo)") - @GetMapping(value = "/api/mobile/versionInfo") - public MobileAppVersionInfo getMobileVersionInfo(@Parameter(description = "Mobile application package name") - @RequestParam String pkgName, - @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) - @RequestParam PlatformType platform) { - MobileApp mobileApp = mobileAppService.findMobileAppByPkgNameAndPlatformType(pkgName, platform); - return mobileApp != null ? mobileApp.getVersionInfo() : null; - } - - private List getVisiblePages(MobileAppBundle mobileAppBundle) throws JsonProcessingException { - if (mobileAppBundle != null && mobileAppBundle.getLayoutConfig() != null) { - List mobilePages = mobileAppBundle.getLayoutConfig().getPages() - .stream() - .filter(MobilePage::isVisible) - .collect(Collectors.toList()); - return JacksonUtil.readValue(JacksonUtil.writeValueAsViewIgnoringNullFields(mobilePages, Views.Public.class), new TypeReference<>() {}); - } else { - return Collections.emptyList(); - } - } - -} diff --git a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java index 0b2cb8fe8f..547cdc738f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java @@ -126,9 +126,9 @@ public class QrCodeSettingsController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PostMapping(value = "/api/mobile/qr/settings") public QrCodeSettings saveQrCodeSettings(@Parameter(description = "A JSON value representing the mobile apps configuration") - @RequestBody QrCodeSettings qrCodeSettings) throws ThingsboardException { + @RequestBody QrCodeSettings qrCodeSettings) throws ThingsboardException { SecurityUser currentUser = getCurrentUser(); - accessControlService.checkPermission(currentUser, Resource.QR_CODE_SETTINGS, Operation.WRITE); + accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.WRITE); qrCodeSettings.setTenantId(getTenantId()); return qrCodeSettingService.saveQrCodeSettings(currentUser.getTenantId(), qrCodeSettings); } @@ -137,9 +137,9 @@ public class QrCodeSettingsController extends BaseController { 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/qr/settings") - public QrCodeSettings getQrAppSettings() throws ThingsboardException { + public QrCodeSettings getQrCodeSettings() throws ThingsboardException { SecurityUser currentUser = getCurrentUser(); - accessControlService.checkPermission(currentUser, Resource.QR_CODE_SETTINGS, Operation.READ); + accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ); return qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java index fc50d85040..46cd1c2979 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java @@ -47,7 +47,7 @@ public class CustomerUserPermissions extends AbstractPermissions { put(Resource.DEVICE_PROFILE, profilePermissionChecker); put(Resource.ASSET_PROFILE, profilePermissionChecker); put(Resource.TB_RESOURCE, customerResourcePermissionChecker); - put(Resource.QR_CODE_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ)); + put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ)); } private static final PermissionChecker customerAlarmPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index dd169b5a6b..8c6f4d00e0 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -50,7 +50,7 @@ public enum Resource { VERSION_CONTROL, NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), - QR_CODE_SETTINGS; + MOBILE_APP_SETTINGS; private final Set entityTypes; Resource() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index 9e4b7d3b1e..b3210ea68a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java @@ -44,7 +44,7 @@ public class SysAdminPermissions extends AbstractPermissions { put(Resource.TB_RESOURCE, systemEntityPermissionChecker); put(Resource.QUEUE, systemEntityPermissionChecker); put(Resource.NOTIFICATION, systemEntityPermissionChecker); - put(Resource.QR_CODE_SETTINGS, PermissionChecker.allowAllPermissionChecker); + put(Resource.MOBILE_APP_SETTINGS, PermissionChecker.allowAllPermissionChecker); } private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index dc77480660..a0ef8a1766 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -50,7 +50,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.QUEUE, queuePermissionChecker); put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker); put(Resource.NOTIFICATION, tenantEntityPermissionChecker); - put(Resource.QR_CODE_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ)); + put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ)); put(Resource.MOBILE_APP, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 4eca2e93ed..d4cc6d13e3 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -610,7 +610,7 @@ cache: 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 qrCodeSettings: - timeToLiveInMinutes: "${CACHE_SPECS_MOBILE_APP_SETTINGS_TTL:1440}" # Mobile application cache TTL + timeToLiveInMinutes: "${CACHE_SPECS_MOBILE_APP_SETTINGS_TTL:1440}" # Qr code settings cache TTL maxSize: "${CACHE_SPECS_MOBILE_APP_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled mobileSecretKey: timeToLiveInMinutes: "${CACHE_MOBILE_SECRET_KEY_TTL:2}" # QR secret key cache TTL diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java index bbc84e732b..87ed77c7c9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java @@ -76,9 +76,6 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements @Override public void deleteMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId) { log.trace("Executing deleteMobileAppBundleById [{}]", mobileAppBundleId.getId()); - mobileAppBundleDao.removeById(tenantId, mobileAppBundleId.getId()); - eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(mobileAppBundleId).build()); - try { mobileAppBundleDao.removeById(tenantId, mobileAppBundleId.getId()); eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(mobileAppBundleId).build()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 9d550ff341..2f3893d405 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -178,7 +178,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService { + UserMobileSessionInfo mobileInfo = findMobileSessionInfo(tenantId, userId).orElseGet(() -> { UserMobileSessionInfo newMobileInfo = new UserMobileSessionInfo(); newMobileInfo.setSessions(new HashMap<>()); return newMobileInfo; @@ -479,12 +479,12 @@ public class UserServiceImpl extends AbstractCachedEntityService findMobileSessions(TenantId tenantId, UserId userId) { - return findMobileInfo(tenantId, userId).map(UserMobileSessionInfo::getSessions).orElse(Collections.emptyMap()); + return findMobileSessionInfo(tenantId, userId).map(UserMobileSessionInfo::getSessions).orElse(Collections.emptyMap()); } @Override public MobileSessionInfo findMobileSession(TenantId tenantId, UserId userId, String mobileToken) { - return findMobileInfo(tenantId, userId).map(mobileInfo -> mobileInfo.getSessions().get(mobileToken)).orElse(null); + return findMobileSessionInfo(tenantId, userId).map(mobileInfo -> mobileInfo.getSessions().get(mobileToken)).orElse(null); } @Override @@ -495,7 +495,7 @@ public class UserServiceImpl extends AbstractCachedEntityService findMobileInfo(TenantId tenantId, UserId userId) { + private Optional findMobileSessionInfo(TenantId tenantId, UserId userId) { return Optional.ofNullable(userSettingsService.findUserSettings(tenantId, userId, UserSettingsType.MOBILE)) .map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileSessionInfo.class)); } From af719050a0cd8fbdeb2f9fc0a13755ad1ad9ce81 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 22 Oct 2024 15:03:48 +0300 Subject: [PATCH 16/48] fixed bundle info when apps are missed --- .../main/data/upgrade/3.8.0/schema_update.sql | 8 ++++--- .../server/controller/OAuth2Controller.java | 21 ++++++++++--------- .../permission/SysAdminPermissions.java | 2 +- .../permission/TenantAdminPermissions.java | 1 + .../MobileAppBundleControllerTest.java | 12 +++++++++++ .../data/mobile/app/MobileAppVersionInfo.java | 4 ++-- .../data/mobile/layout/CustomMobilePage.java | 2 +- .../sql/mobile/MobileAppBundleRepository.java | 6 ++++-- .../main/resources/sql/schema-entities.sql | 2 +- 9 files changed, 38 insertions(+), 20 deletions(-) diff --git a/application/src/main/data/upgrade/3.8.0/schema_update.sql b/application/src/main/data/upgrade/3.8.0/schema_update.sql index 8884919cc5..6d71efd831 100644 --- a/application/src/main/data/upgrade/3.8.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.0/schema_update.sql @@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS mobile_app_bundle ( ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS platform_type varchar(32), ADD COLUMN IF NOT EXISTS status varchar(32), - ADD COLUMN IF NOT EXISTS version_info varchar(16384), + ADD COLUMN IF NOT EXISTS version_info varchar(100000), ADD COLUMN IF NOT EXISTS store_info varchar(16384), DROP CONSTRAINT IF EXISTS mobile_app_pkg_name_key, DROP CONSTRAINT IF EXISTS mobile_app_unq_key; @@ -70,12 +70,14 @@ $$ -- duplicate app for iOS platform type iosAppId := uuid_generate_v4(); INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, app_secret, platform_type, status) - VALUES (iosAppId, (extract(epoch from now()) * 1000), mobileAppRecord.tenant_id, mobileAppRecord.pkg_name, mobileAppRecord.app_secret, 'IOS', mobileAppRecord.status); + VALUES (iosAppId, (extract(epoch from now()) * 1000), mobileAppRecord.tenant_id, mobileAppRecord.pkg_name, mobileAppRecord.app_secret, 'IOS', mobileAppRecord.status) + ON CONFLICT DO NOTHING; -- create bundle for android and iOS app generatedBundleId := uuid_generate_v4(); INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id, ios_app_id, oauth2_enabled) VALUES (generatedBundleId, (extract(epoch from now()) * 1000), mobileAppRecord.tenant_id, - 'Autogenerated for ' || mobileAppRecord.pkg_name, mobileAppRecord.id, iosAppId, mobileAppRecord.oauth2_enabled); + 'Autogenerated for ' || mobileAppRecord.pkg_name, mobileAppRecord.id, iosAppId, mobileAppRecord.oauth2_enabled) + ON CONFLICT DO NOTHING; UPDATE mobile_app_bundle_oauth2_client SET mobile_app_bundle_id = generatedBundleId WHERE mobile_app_bundle_id = mobileAppRecord.id; END LOOP; END IF; diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index 8a8055c4af..a235034756 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -59,6 +59,7 @@ import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DE import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; @RestController @TbCoreComponent @@ -104,8 +105,8 @@ public class OAuth2Controller extends BaseController { } } - @ApiOperation(value = "Save OAuth2 Client (saveOAuth2Client)", notes = SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @ApiOperation(value = "Save OAuth2 Client (saveOAuth2Client)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PostMapping(value = "/oauth2/client") public OAuth2Client saveOAuth2Client(@RequestBody @Valid OAuth2Client oAuth2Client) throws Exception { TenantId tenantId = getTenantId(); @@ -114,8 +115,8 @@ public class OAuth2Controller extends BaseController { return tbOauth2ClientService.save(oAuth2Client, getCurrentUser()); } - @ApiOperation(value = "Get OAuth2 Client infos (findTenantOAuth2ClientInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @ApiOperation(value = "Get OAuth2 Client infos (findTenantOAuth2ClientInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/oauth2/client/infos") public PageData findTenantOAuth2ClientInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -134,7 +135,7 @@ public class OAuth2Controller extends BaseController { @ApiOperation(value = "Get OAuth2 Client infos By Ids (findTenantOAuth2ClientInfosByIds)", notes = "Fetch OAuth2 Client info objects based on the provided ids. ") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/oauth2/client/infos", params = {"clientIds"}) public List findTenantOAuth2ClientInfosByIds( @Parameter(description = "A list of oauth2 ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @@ -143,8 +144,8 @@ public class OAuth2Controller extends BaseController { return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds); } - @ApiOperation(value = "Get OAuth2 Client by id (getOAuth2ClientById)", notes = SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @ApiOperation(value = "Get OAuth2 Client by id (getOAuth2ClientById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/oauth2/client/{id}") public OAuth2Client getOAuth2ClientById(@PathVariable UUID id) throws ThingsboardException { OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(id); @@ -152,8 +153,8 @@ public class OAuth2Controller extends BaseController { } @ApiOperation(value = "Delete oauth2 client (deleteOauth2Client)", - notes = "Deletes the oauth2 client. Referencing non-existing oauth2 client Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('SYS_ADMIN')") + notes = "Deletes the oauth2 client. Referencing non-existing oauth2 client Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @DeleteMapping(value = "/oauth2/client/{id}") public void deleteOauth2Client(@PathVariable UUID id) throws Exception { OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(id); @@ -165,7 +166,7 @@ public class OAuth2Controller extends BaseController { "double quotes. After successful authentication with OAuth2 provider, it makes a redirect to this path so that the platform can do " + "further log in processing. This URL may be configured as 'security.oauth2.loginProcessingUrl' property in yml configuration file, or " + "as 'SECURITY_OAUTH2_LOGIN_PROCESSING_URL' env variable. By default it is '/login/oauth2/code/'" + SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/oauth2/loginProcessingUrl") public String getLoginProcessingUrl() throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CLIENT, Operation.READ); diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index b3210ea68a..875976465d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java @@ -35,7 +35,7 @@ public class SysAdminPermissions extends AbstractPermissions { put(Resource.USER, userPermissionChecker); put(Resource.WIDGETS_BUNDLE, systemEntityPermissionChecker); put(Resource.WIDGET_TYPE, systemEntityPermissionChecker); - put(Resource.OAUTH2_CLIENT, PermissionChecker.allowAllPermissionChecker); + put(Resource.OAUTH2_CLIENT, systemEntityPermissionChecker); put(Resource.MOBILE_APP, systemEntityPermissionChecker); put(Resource.MOBILE_APP_BUNDLE, systemEntityPermissionChecker); put(Resource.DOMAIN, PermissionChecker.allowAllPermissionChecker); diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index a0ef8a1766..c63121e60b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -51,6 +51,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker); put(Resource.NOTIFICATION, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ)); + put(Resource.OAUTH2_CLIENT, tenantEntityPermissionChecker); put(Resource.MOBILE_APP, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); } diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java index 9e7204a355..1030a6bd9c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -99,6 +100,17 @@ public class MobileAppBundleControllerTest extends AbstractControllerTest { assertThat(createdMobileAppBundle.getIosAppId()).isEqualTo(iosApp.getId()); } + @Test + public void testSaveMobileAppBundleWithoutApps() throws Exception { + MobileAppBundle mobileAppBundle = new MobileAppBundle(); + mobileAppBundle.setTitle("Test bundle"); + + MobileAppBundle savedAppBundle = doPost("/api/mobile/bundle", mobileAppBundle, MobileAppBundle.class); + MobileAppBundleInfo retrievedMobileAppBundleInfo = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedAppBundle.getId().getId()); + assertThat(retrievedMobileAppBundleInfo).isEqualTo(new MobileAppBundleInfo(savedAppBundle, null, null, false, + Collections.emptyList())); + } + @Test public void testUpdateMobileAppBundleOauth2Clients() throws Exception { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppVersionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppVersionInfo.java index e177aed4a5..2ed6e962ec 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppVersionInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppVersionInfo.java @@ -35,7 +35,7 @@ public class MobileAppVersionInfo { private String minVersion; @Schema(description = "Release notes of minimum supported version") - @Length(fieldName = "minVersionReleaseNotes", max = 10000) + @Length(fieldName = "minVersionReleaseNotes", max = 40000) private String minVersionReleaseNotes; @Schema(description = "Latest supported version") @@ -43,7 +43,7 @@ public class MobileAppVersionInfo { private String latestVersion; @Schema(description = "Release notes of latest supported version") - @Length(fieldName = "latestVersionReleaseNotes", max = 10000) + @Length(fieldName = "latestVersionReleaseNotes", max = 40000) private String latestVersionReleaseNotes; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java index 114e5930d3..d2e4db347c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.Views; @EqualsAndHashCode(callSuper = true) public class CustomMobilePage extends AbstractMobilePage { - @Schema(description = "Path", example = "") + @Schema(description = "Path to custom page", example = "/alarmDetails/868c7083-032d-4f52-b8b4-7859aebb6a4e") @JsonView(Views.Public.class) private String path; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java index 982ff3c9ca..9aff6af3f1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java @@ -30,7 +30,8 @@ import java.util.UUID; public interface MobileAppBundleRepository extends JpaRepository { - @Query("SELECT new org.thingsboard.server.dao.model.sql.MobileAppBundleInfoEntity(b, andApp.pkgName, iosApp.pkgName, (andApp.status = 'PUBLISHED' or iosApp.status = 'PUBLISHED')) " + + @Query("SELECT new org.thingsboard.server.dao.model.sql.MobileAppBundleInfoEntity(b, andApp.pkgName, iosApp.pkgName, " + + "((andApp.status IS NOT NULL AND andApp.status = 'PUBLISHED') OR (iosApp.status IS NOT NULL AND iosApp.status = 'PUBLISHED'))) " + "FROM MobileAppBundleEntity b " + "LEFT JOIN MobileAppEntity andApp on b.androidAppId = andApp.id " + "LEFT JOIN MobileAppEntity iosApp on b.iosAppID = iosApp.id " + @@ -40,7 +41,8 @@ public interface MobileAppBundleRepository extends JpaRepository Date: Tue, 22 Oct 2024 15:35:32 +0300 Subject: [PATCH 17/48] UI: Add mobile bundles table --- .../mobile/layout/AbstractMobilePage.java | 2 +- .../data/mobile/layout/DefaultPageId.java | 4 +- ui-ngx/src/app/core/http/entity.service.ts | 13 +- .../src/app/core/http/mobile-app.service.ts | 26 +- ui-ngx/src/app/core/services/menu.models.ts | 14 +- .../entity/entities-table.component.html | 2 +- .../entity/entity-chips.component.ts | 51 +-- .../applications/applications.module.ts | 11 +- .../mobile-app-dialog.component.html | 49 +++ .../mobile-app-dialog.component.ts | 81 +++++ .../mobile-app-table-header.component.html | 2 +- .../applications/mobile-app.component.ts | 6 +- .../mobile/bundes/bundles-routing.module.ts | 48 +++ .../pages/mobile/bundes/bundles.module.ts | 49 +++ .../add-mobile-page-dialog.component.html | 44 +++ .../add-mobile-page-dialog.component.scss | 29 ++ .../add-mobile-page-dialog.component.ts | 53 +++ .../custom-mobile-page-panel.component.html | 39 +++ .../custom-mobile-page-panel.component.scss | 49 +++ .../custom-mobile-page-panel.component.ts | 64 ++++ .../layout/custom-mobile-page.component.html | 68 ++++ .../layout/custom-mobile-page.component.scss | 22 ++ .../layout/custom-mobile-page.component.ts | 132 +++++++ .../default-mobile-page-panel.component.html | 64 ++++ .../default-mobile-page-panel.component.scss | 55 +++ .../default-mobile-page-panel.component.ts | 120 +++++++ .../layout/mobile-layout.component.html | 108 ++++++ .../layout/mobile-layout.component.scss | 190 +++++++++++ .../bundes/layout/mobile-layout.component.ts | 245 +++++++++++++ .../mobile-page-item-row.component.html | 75 ++++ .../mobile-page-item-row.component.scss | 84 +++++ .../layout/mobile-page-item-row.component.ts | 323 ++++++++++++++++++ .../mobile-bundle-dialog.component.html | 109 ++++++ .../mobile-bundle-dialog.component.scss | 73 ++++ .../bundes/mobile-bundle-dialog.component.ts | 226 ++++++++++++ .../mobile-bundle-table-config.resolve.ts | 142 ++++++++ .../mobile-bundle-table-header.component.html | 28 ++ .../mobile-bundle-table-header.component.scss | 23 ++ .../mobile-bundle-table-header.component.ts | 37 ++ .../pages/mobile/mobile-routing.module.ts | 2 + .../home/pages/mobile/mobile.module.ts | 6 +- .../entity/entity-autocomplete.component.html | 6 + .../entity/entity-autocomplete.component.ts | 18 + .../entity/entity-list.component.html | 2 +- .../entity/entity-list.component.ts | 5 +- .../app/shared/models/entity-type.models.ts | 21 +- .../app/shared/models/mobile-app.models.ts | 189 +++++++++- .../assets/locale/locale.constant-en_US.json | 55 ++- 48 files changed, 3002 insertions(+), 62 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/bundles-routing.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/bundles.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.ts diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java index 4d87530d0e..9a97b737f0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.Views; @Data public abstract class AbstractMobilePage implements MobilePage { - @Schema(description = "Page label", example = "Air quality", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "Page label", example = "Air quality") @JsonView(Views.Public.class) protected String label; @Schema(description = "Indicates if page is visible", example = "true", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultPageId.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultPageId.java index 20c79688f0..3409fec793 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultPageId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultPageId.java @@ -23,5 +23,7 @@ public enum DefaultPageId { CUSTOMERS, ASSETS, AUDIT_LOGS, - NOTIFICATIONS + NOTIFICATIONS, + DEVICE_LIST, + DASHBOARDS } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 50978662df..373fa2c15c 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -97,6 +97,8 @@ import { UserId } from '@shared/models/id/user-id'; import { AlarmService } from '@core/http/alarm.service'; import { ResourceService } from '@core/http/resource.service'; import { OAuth2Service } from '@core/http/oauth2.service'; +import { MobileAppService } from '@core/http/mobile-app.service'; +import { PlatformType } from '@shared/models/oauth2.models'; @Injectable({ providedIn: 'root' @@ -127,7 +129,8 @@ export class EntityService { private notificationService: NotificationService, private alarmService: AlarmService, private resourceService: ResourceService, - private oauth2Service: OAuth2Service + private oauth2Service: OAuth2Service, + private mobileAppService: MobileAppService, ) { } private getEntityObservable(entityType: EntityType, entityId: string, @@ -173,6 +176,10 @@ export class EntityService { break; case EntityType.QUEUE_STATS: observable = this.queueService.getQueueStatisticsById(entityId, config); + break; + case EntityType.MOBILE_APP: + observable = this.mobileAppService.getMobileAppInfoById(entityId, config); + break; } return observable; } @@ -462,6 +469,10 @@ export class EntityService { pageLink.sortOrder.property = 'title'; entitiesObservable = this.oauth2Service.findTenantOAuth2ClientInfos(pageLink, config); break; + case EntityType.MOBILE_APP: + pageLink.sortOrder.property = 'pkgName'; + entitiesObservable = this.mobileAppService.getTenantMobileAppInfos(pageLink, subType as PlatformType, config); + break; } return entitiesObservable; } diff --git a/ui-ngx/src/app/core/http/mobile-app.service.ts b/ui-ngx/src/app/core/http/mobile-app.service.ts index ac4756f26f..37470768ca 100644 --- a/ui-ngx/src/app/core/http/mobile-app.service.ts +++ b/ui-ngx/src/app/core/http/mobile-app.service.ts @@ -21,6 +21,7 @@ import { Observable } from 'rxjs'; import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; import { MobileApp, MobileAppBundle, MobileAppBundleInfo } from '@shared/models/mobile-app.models'; +import { PlatformType } from '@shared/models/oauth2.models'; @Injectable({ providedIn: 'root' @@ -36,8 +37,12 @@ export class MobileAppService { return this.http.post(`/api/mobile/app`, mobileApp, defaultHttpOptionsFromConfig(config)); } - public getTenantMobileAppInfos(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/mobile/app${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + public getTenantMobileAppInfos(pageLink: PageLink, platformType?: PlatformType, config?: RequestConfig): Observable> { + let url = `/api/mobile/app${pageLink.toQuery()}`; + if (platformType) { + url += `&platformType=${platformType}` + } + return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } public getMobileAppInfoById(id: string, config?: RequestConfig): Observable { @@ -49,12 +54,15 @@ export class MobileAppService { } public saveMobileAppBundle(mobileAppBundle: MobileAppBundle, oauth2ClientIds?: Array, config?: RequestConfig) { - return this.http.post(`/api/mobile/bundle${oauth2ClientIds ? '?oauth2ClientIds=' + oauth2ClientIds.join(',') : ''}`, - mobileAppBundle, defaultHttpOptionsFromConfig(config)); + let url = '/api/mobile/bundle'; + if (oauth2ClientIds?.length) { + url += `?oauth2ClientIds=${oauth2ClientIds.join(',')}`; + } + return this.http.post(url, mobileAppBundle, defaultHttpOptionsFromConfig(config)); } - public updateOauth2Clients(id: string, oauth2ClientIds?: Array, config?: RequestConfig) { - return this.http.put(`/mobile/bundle/${id}/oauth2Clients`, oauth2ClientIds, defaultHttpOptionsFromConfig(config)); + public updateOauth2Clients(id: string, oauth2ClientIds: Array, config?: RequestConfig) { + return this.http.put(`/api/mobile/bundle/${id}/oauth2Clients`, oauth2ClientIds, defaultHttpOptionsFromConfig(config)); } public getTenantMobileAppBundleInfos(pageLink: PageLink, config?: RequestConfig): Observable> { @@ -62,7 +70,11 @@ export class MobileAppService { } public getMobileAppBundleInfoById(id: string, config?: RequestConfig): Observable { - return this.http.get(`/api/mobile/bundle/infos/${id}`, defaultHttpOptionsFromConfig(config)); + return this.http.get(`/api/mobile/bundle/info/${id}`, defaultHttpOptionsFromConfig(config)); + } + + public deleteMobileAppBundle(id: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/mobile/bundle/${id}`, defaultHttpOptionsFromConfig(config)); } } diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index debaefe882..0a80f4e1de 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -67,6 +67,7 @@ export enum MenuId { notification_rules = 'notification_rules', mobile_center = 'mobile_center', mobile_apps = 'mobile_apps', + mobile_bundles = 'mobile_bundles', mobile_app_settings = 'mobile_app_settings', settings = 'settings', general = 'general', @@ -292,6 +293,16 @@ export const menuSectionMap = new Map([ icon: 'list' } ], + [ + MenuId.mobile_bundles, + { + id: MenuId.mobile_bundles, + name: 'mobile.bundles', + type: 'link', + path: '/mobile-center/bundles', + icon: 'mdi:package' + } + ], [ MenuId.mobile_app_settings, { @@ -702,6 +713,7 @@ const defaultUserMenuMap = new Map([ id: MenuId.mobile_center, pages: [ {id: MenuId.mobile_apps}, + {id: MenuId.mobile_bundles}, {id: MenuId.mobile_app_settings} ] }, @@ -850,7 +862,7 @@ const defaultHomeSectionMap = new Map([ { name: 'admin.system-settings', places: [MenuId.general, MenuId.mail_server, - MenuId.notification_settings, MenuId.security_settings, MenuId.oauth2, MenuId.domains, MenuId.mobile_apps, + MenuId.notification_settings, MenuId.security_settings, MenuId.oauth2, MenuId.domains, MenuId.clients, MenuId.two_fa, MenuId.resources_library, MenuId.queues] } ] diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index 1700938611..f6d2773637 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -173,7 +173,7 @@ (click)="$event.stopPropagation();"> - + diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.ts index cd9be9d79d..cee82995aa 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.ts @@ -14,48 +14,49 @@ /// limitations under the License. /// -import { Component, Input } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { BaseData } from '@shared/models/base-data'; import { EntityId } from '@shared/models/id/entity-id'; import { baseDetailsPageByEntityType, EntityType } from '@app/shared/public-api'; - -const entityTypeEntitiesPropertyKeyMap = new Map([ - [EntityType.DOMAIN, 'oauth2ClientInfos'], - [EntityType.MOBILE_APP, 'oauth2ClientInfos'] -]); +import { isObject } from '@core/utils'; @Component({ selector: 'tb-entity-chips', templateUrl: './entity-chips.component.html', styleUrls: ['./entity-chips.component.scss'] }) -export class EntityChipsComponent { +export class EntityChipsComponent implements OnChanges { @Input() - set entity(value: BaseData) { - this.entityValue = value; - this.update(); - } + entity: BaseData; - get entity(): BaseData { - return this.entityValue; - } + @Input() + key: string; entityDetailsPrefixUrl: string; - subEntities: Array>; - - private entityValue?: BaseData; + subEntities: Array> = []; - private subEntitiesKey: string; + ngOnChanges(changes: SimpleChanges) { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (propName === 'entity' && change.currentValue !== change.previousValue) { + this.update(); + } + } + } - update(): void { - if (this.entity && this.entity.id) { - const entityType = this.entity.id.entityType as EntityType; - this.subEntitiesKey = entityTypeEntitiesPropertyKeyMap.get(entityType); - this.subEntities = this.entity?.[this.subEntitiesKey]; - if (this.subEntities.length) { - this.entityDetailsPrefixUrl = baseDetailsPageByEntityType.get(this.subEntities[0].id.entityType as EntityType); + private update(): void { + if (this.entity && this.entity.id && this.key) { + let entitiesList = this.entity?.[this.key]; + if (isObject(entitiesList) && !Array.isArray(entitiesList)) { + entitiesList = [entitiesList]; + } + if (Array.isArray(entitiesList)) { + this.subEntities = entitiesList; + if (this.subEntities.length) { + this.entityDetailsPrefixUrl = baseDetailsPageByEntityType.get(this.subEntities[0].id.entityType as EntityType); + } } } } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts index a3a05d6a50..eae2e5e278 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts @@ -22,18 +22,23 @@ import { SharedModule } from '@shared/shared.module'; import { HomeComponentsModule } from '@home/components/home-components.module'; import { ApplicationsRoutingModule } from '@home/pages/mobile/applications/applications-routing.module'; import { ReleaseNotesPanelComponent } from '@home/pages/mobile/applications/release-notes-panel.component'; +import { MobileAppDialogComponent } from '@home/pages/mobile/applications/mobile-app-dialog.component'; @NgModule({ declarations: [ MobileAppComponent, MobileAppTableHeaderComponent, - ReleaseNotesPanelComponent + ReleaseNotesPanelComponent, + MobileAppDialogComponent, ], imports: [ CommonModule, SharedModule, HomeComponentsModule, - ApplicationsRoutingModule + ApplicationsRoutingModule, + ], + exports: [ + MobileAppDialogComponent, ] }) -export class ApplicationModule { } +export class MobileApplicationModule { } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html new file mode 100644 index 0000000000..f3b0e82aca --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html @@ -0,0 +1,49 @@ + +
+ +

{{ 'mobile.add-application' | translate }}

+ +
+ +
+ + +
+
+ +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts new file mode 100644 index 0000000000..193fc4bbb5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts @@ -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. +/// + +import { AfterViewInit, Component, Inject, OnDestroy, SkipSelf, ViewChild } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms'; +import { MobileApp } from '@shared/models/mobile-app.models'; +import { MobileAppComponent } from '@home/pages/mobile/applications/mobile-app.component'; +import { PlatformType } from '@shared/models/oauth2.models'; +import { MobileAppService } from '@core/http/mobile-app.service'; + +export interface MobileAppDialogData { + platformType: PlatformType; +} + +@Component({ + selector: 'tb-mobile-app-dialog', + templateUrl: './mobile-app-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: MobileAppDialogComponent}], + styleUrls: [] +}) +export class MobileAppDialogComponent extends DialogComponent implements OnDestroy, AfterViewInit, ErrorStateMatcher { + + submitted = false; + + @ViewChild('mobileAppComponent', {static: true}) mobileAppComponent: MobileAppComponent; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + private mobileAppService: MobileAppService, + @Inject(MAT_DIALOG_DATA) public data: MobileAppDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher) { + super(store, router, dialogRef); + } + + ngAfterViewInit(): void { + setTimeout(() => { + this.mobileAppComponent.entityForm.markAsDirty(); + this.mobileAppComponent.entityForm.patchValue({platformType: this.data.platformType}); + this.mobileAppComponent.entityForm.get('platformType').disable({emitEvent: false}); + }, 0); + } + + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save() { + this.submitted = true; + if (this.mobileAppComponent.entityForm.valid) { + this.mobileAppService.saveMobileApp(this.mobileAppComponent.entityFormValue()).subscribe( + app => this.dialogRef.close(app) + ) + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.html index 1783b2b4dd..c919bb4a5d 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.html @@ -18,7 +18,7 @@
mobile.applications
- +
+ +
+ + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.scss new file mode 100644 index 0000000000..32073aa85c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.scss @@ -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. + */ +:host { + width: 600px; + height: 100%; + max-width: 100%; + max-height: 100vh; + display: grid; + grid-template-rows: min-content minmax(auto, 1fr) min-content; + + .mobile-page-form { + display: flex; + flex-direction: column; + gap: 16px; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.ts new file mode 100644 index 0000000000..0183a1b118 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.ts @@ -0,0 +1,53 @@ +/// +/// 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. +/// + +import { Component } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { CustomMobilePage } from '@shared/models/mobile-app.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder } from '@angular/forms'; +import { deepTrim } from '@core/utils'; + +@Component({ + selector: 'tb-add-mobile-page-dialog', + templateUrl: './add-mobile-page-dialog.component.html', + styleUrls: ['./add-mobile-page-dialog.component.scss'] +}) +export class AddMobilePageDialogComponent extends DialogComponent { + + customMobilePage = this.fb.control(null); + + constructor(protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save() { + if (this.customMobilePage.valid) { + const pageItem = deepTrim(this.customMobilePage.value); + this.dialogRef.close(pageItem); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.html new file mode 100644 index 0000000000..638dfceec8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.html @@ -0,0 +1,39 @@ + +
+
mobile.edit-custom-page
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.scss new file mode 100644 index 0000000000..fdd0108fca --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.scss @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../scss/constants'; + +.tb-custom-mobile-page-panel { + width: 500px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-custom-mobile-page-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-custom-mobile-page-panel-content { + display: flex; + flex-direction: column; + gap: 16px; + overflow: auto; + margin: -10px; + padding: 10px; + } + .tb-custom-mobile-page-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.ts new file mode 100644 index 0000000000..99f783ef48 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.ts @@ -0,0 +1,64 @@ +/// +/// 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. +/// + +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { CustomMobilePage } from '@shared/models/mobile-app.models'; +import { TbPopoverComponent } from '@shared/components/popover.component'; + +@Component({ + selector: 'tb-custom-menu-item-panel', + templateUrl: './custom-mobile-page-panel.component.html', + styleUrls: ['./custom-mobile-page-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class CustomMobilePagePanelComponent implements OnInit { + + @Input() + disabled: boolean; + + @Input() + pageItem: CustomMobilePage; + + @Input() + popover: TbPopoverComponent; + + @Output() + customMobilePageApplied = new EventEmitter(); + + mobilePageControl = this.fb.control(null); + + constructor(private fb: FormBuilder) { + } + + ngOnInit() { + this.mobilePageControl.setValue(this.pageItem, {emitEvent: false}); + if (this.disabled) { + this.mobilePageControl.disable({emitEvent: false}); + } + } + + cancel() { + this.popover?.hide(); + } + + apply() { + if (this.mobilePageControl.valid) { + const menuItem = this.mobilePageControl.value; + this.customMobilePageApplied.emit(menuItem); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html new file mode 100644 index 0000000000..f56fbe3a46 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html @@ -0,0 +1,68 @@ + +
+
+ + {{ 'mobile.visible' | translate }} + +
+
+ + + + mobile.page-name + + +
+ + mobile.page-type + + + {{ mobilePageTypeTranslations.get(pageType) | translate }} + + + + + + + + + mobile.url + + + + + + mobile.path + + + +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.scss new file mode 100644 index 0000000000..5fda87092d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.scss @@ -0,0 +1,22 @@ +/** + * 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. + */ +:host{ + .mobile-page-form { + display: flex; + flex-direction: column; + gap: 16px; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts new file mode 100644 index 0000000000..df683cf572 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts @@ -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. +/// + +import { booleanAttribute, Component, forwardRef, Input } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { CustomMobilePage, MobilePageType, mobilePageTypeTranslations } from '@shared/models/mobile-app.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-mobile-page-item', + templateUrl: './custom-mobile-page.component.html', + styleUrls: ['./custom-mobile-page.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CustomMobilePageComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CustomMobilePageComponent), + multi: true + } + ] +}) +export class CustomMobilePageComponent implements ControlValueAccessor, Validator { + + @Input({transform: booleanAttribute}) + disabled: boolean; + + mobilePagesTypes = [MobilePageType.DASHBOARD, MobilePageType.WEB_VIEW, MobilePageType.CUSTOM]; + MobilePageType = MobilePageType; + mobilePageTypeTranslations = mobilePageTypeTranslations; + + customMobilePageForm = this.fb.group({ + visible: [true], + icon: ['star'], + label: ['', Validators.required], + type: [MobilePageType.DASHBOARD], + dashboardId: ['', Validators.required], + url: [{value:'', disabled: true}, [Validators.required]], + path: [{value:'', disabled: true}, [Validators.required, Validators.pattern(/^((?!\s).)*$/)]] + }); + + private propagateChange = (_val: any) => {}; + + constructor(private fb: FormBuilder, + private store: Store) { + this.customMobilePageForm.get('type').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(type => { + this.customMobilePageForm.get('dashboardId').disable({emitEvent: false}); + this.customMobilePageForm.get('url').disable({emitEvent: false}); + this.customMobilePageForm.get('path').disable({emitEvent: false}); + switch (type) { + case MobilePageType.DASHBOARD: + this.customMobilePageForm.get('dashboardId').enable({emitEvent: false}); + break; + case MobilePageType.WEB_VIEW: + this.customMobilePageForm.get('url').enable({emitEvent: false}); + break; + case MobilePageType.CUSTOM: + this.customMobilePageForm.get('path').enable({emitEvent: false}); + break; + } + }); + + if (getCurrentAuthUser(this.store).authority === Authority.SYS_ADMIN) { + this.mobilePagesTypes.shift(); + this.customMobilePageForm.get('type').setValue(MobilePageType.WEB_VIEW); + } + + this.customMobilePageForm.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value) => this.propagateChange(value)) + } + + registerOnChange(fn: any) { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean) { + this.disabled = isDisabled; + if (isDisabled) { + this.customMobilePageForm.disable({emitEvent: false}); + } else { + this.customMobilePageForm.enable({emitEvent: false}); + this.customMobilePageForm.get('type').updateValueAndValidity({onlySelf: true}); + } + } + + validate(): ValidationErrors | null { + if (!this.customMobilePageForm.valid) { + return { + invalidCustomMobilePageForm: true + }; + } + return null; + } + + writeValue(value: CustomMobilePage) { + this.customMobilePageForm.patchValue(value, {emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html new file mode 100644 index 0000000000..8345facaed --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html @@ -0,0 +1,64 @@ + +
+
+
mobile.edit-page
+ +
+
+
+ + {{ 'mobile.visible' | translate }} + +
+
+ + + + mobile.page-name + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.scss new file mode 100644 index 0000000000..a8435cb97e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.scss @@ -0,0 +1,55 @@ +/** + * 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. + */ +@import '../scss/constants'; + +.tb-default-mobile-page-panel { + width: 500px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-default-mobile-page-title-container { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + } + .tb-default-mobile-page-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-default-mobile-page-panel-content { + display: flex; + flex-direction: column; + gap: 16px; + overflow: auto; + margin: -10px; + padding: 10px; + } + .tb-default-mobile-page-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.ts new file mode 100644 index 0000000000..f42cedd942 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.ts @@ -0,0 +1,120 @@ +/// +/// 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. +/// + +import { Component, DestroyRef, EventEmitter, inject, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { DefaultMobilePage, defaultMobilePageMap, hideDefaultMenuItems } from '@shared/models/mobile-app.models'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { FormBuilder } from '@angular/forms'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +@Component({ + selector: 'tb-default-mobile-page-panel', + templateUrl: './default-mobile-page-panel.component.html', + styleUrls: ['./default-mobile-page-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class DefaultMobilePagePanelComponent implements OnInit { + + @Input() + disabled: boolean; + + @Input() + pageItem: DefaultMobilePage; + + @Input() + popover: TbPopoverComponent; + + @Output() + defaultMobilePageApplied = new EventEmitter(); + + mobilePageFormGroup = this.fb.group({ + visible: [true], + icon: [''], + label: [''], + }); + + isCleanupEnabled = false; + defaultItemName: string; + + private defaultMobilePages: Omit; + private destroyRef = inject(DestroyRef); + + constructor(private fb: FormBuilder) { + } + + ngOnInit() { + this.defaultMobilePages = defaultMobilePageMap.get(this.pageItem.id); + this.defaultItemName = this.defaultMobilePages.label; + + this.mobilePageFormGroup.patchValue({ + label: this.pageItem.label, + icon: this.pageItem.icon ? this.pageItem.icon : this.defaultMobilePages.icon, + visible: this.pageItem.visible + }, {emitEvent: false}); + + if (this.disabled) { + this.mobilePageFormGroup.disable({emitEvent: false}); + } else { + this.mobilePageFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + this.updateCleanupState(); + } + } + + cancel() { + this.popover?.hide(); + } + + apply() { + this.defaultMobilePageApplied.emit(this.pageItem); + } + + cleanup() { + this.mobilePageFormGroup.patchValue({ + visible: !hideDefaultMenuItems.includes(this.pageItem.id), + icon: this.defaultMobilePages.icon, + label: null + }, {emitEvent: false}); + this.mobilePageFormGroup.markAsDirty(); + this.updateModel(); + } + + private updateCleanupState() { + this.isCleanupEnabled = (hideDefaultMenuItems.includes(this.pageItem.id) ? this.pageItem.visible : !this.pageItem.visible) || + !!this.pageItem.label || + !!this.pageItem.icon; + } + + private updateModel() { + this.pageItem.visible = this.mobilePageFormGroup.get('visible').value; + const label = this.mobilePageFormGroup.get('label').value; + if (label) { + this.pageItem.label = label; + } else { + delete this.pageItem.label; + } + const icon = this.mobilePageFormGroup.get('icon').value; + if (icon !== this.defaultMobilePages.icon) { + this.pageItem.icon = icon; + } else { + delete this.pageItem.icon; + } + this.updateCleanupState(); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html new file mode 100644 index 0000000000..4bc2f90b44 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html @@ -0,0 +1,108 @@ + +
+
+
+ mobile.pages +
+ + + + + +
+
+
+
+
+
+ {{ 'mobile.custom-page' | translate }} +
+ + + +
+ +
+
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.scss new file mode 100644 index 0000000000..4105d117ad --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.scss @@ -0,0 +1,190 @@ +/** + * 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. + */ +@import './../scss/constants'; + +:host{ + .mobile-layout-container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + + .header { + z-index: 1; + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + + .title-section { + display: flex; + flex-direction: row; + align-items: center; + gap: 24px; + min-width: 0; + .title-container { + font-size: 20px; + letter-spacing: .1px; + } + } + } + + .tb-mobile-items-container { + display: flex; + flex-direction: column; + gap: 4px; + min-height: 0; + overflow: auto; + padding-top: 8px; + padding-bottom: 8px; + } + } + + .tb-mobile-layout-button.mdc-button { + min-width: 120px; + tb-icon { + min-width: 18px; + } + .mdc-button__label { + overflow: hidden; + } + } + + ::ng-deep { + .mat-mdc-button.mat-mdc-button-base.tb-add-mobile-item-button { + height: 24px; + opacity: 0; + transition: all 200ms; + &.divider { + opacity: 1; + .mat-mdc-button-persistent-ripple { + border-bottom-style: dashed; + border-bottom-width: 1px; + } + .tb-add-mobile-label-container { + vertical-align: middle; + .mat-icon { + display: none; + } + } + &:hover { + .mat-mdc-button-persistent-ripple { + border-bottom-style: inherit; + border-bottom-width: inherit; + } + .tb-add-mobile-label-container { + vertical-align: sub; + .mat-icon { + display: inline-block; + } + .tb-mobile-divider-label { + display: none; + } + } + } + .mat-mdc-button-persistent-ripple { + top: 11px; + bottom: 11px; + &:before { + opacity: 0.32; + } + } + } + &:hover { + opacity: 1; + } + .mat-mdc-button-persistent-ripple { + top: 11px; + bottom: 11px; + &:before { + opacity: 0.32; + } + } + &:active { + .mat-mdc-button-persistent-ripple { + &:before { + opacity: 0.48; + } + } + .tb-add-mobile-label-container { + &:after { + opacity: 0.48; + } + } + } + .tb-add-mobile-label-container { + display: inline-flex; + padding: 3px; + vertical-align: sub; + border-radius: 6px; + background: #fff; + .mat-icon { + margin: 0; + } + .tb-mobile-divider-label { + line-height: 12px; + font-size: 10px; + font-weight: 400; + padding: 0 6px; + } + &:after { + display: block; + height: auto; + content: ""; + position: absolute; + inset: 0; + opacity: 0.32; + border-radius: 6px; + border: 1px solid $tb-primary-color; + pointer-events: none; + } + } + } + } +} + +.tb-mobile-item-row-container { + display: flex; + flex-direction: column; + gap: 4px; +} + +.tb-mobile-item-row { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + padding: 12px 12px 12px 0; + background: #fff; + gap: 0; + border-radius: 10px; + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 5px 16px 0 rgba(0, 0, 0, 0.04); + .tb-mobile-item-label { + position: absolute; + left: 8px; + top: -8px; + padding-left: 8px; + padding-right: 8px; + background: #fff; + font-size: 12px; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.4px; + color: rgba(0, 0, 0, 0.54); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.ts new file mode 100644 index 0000000000..3d3f01cee1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.ts @@ -0,0 +1,245 @@ +/// +/// 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. +/// + +import { booleanAttribute, Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator +} from '@angular/forms'; +import { + CustomMobilePage, + getDefaultMobileMenuItem, + isDefaultMobilePagesConfig, + MobileLayoutConfig, + mobileMenuDividers, + MobilePage, + MobilePageType +} from '@shared/models/mobile-app.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { deepClone, isDefined } from '@core/utils'; +import { Subject } from 'rxjs'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { MatDialog } from '@angular/material/dialog'; +import { AddMobilePageDialogComponent } from '@home/pages/mobile/bundes/layout/add-mobile-page-dialog.component'; + +@Component({ + selector: 'tb-mobile-layout', + templateUrl: './mobile-layout.component.html', + styleUrls: ['./mobile-layout.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MobileLayoutComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MobileLayoutComponent), + multi: true + } + ], +}) +export class MobileLayoutComponent implements ControlValueAccessor, Validator { + + @Input({transform: booleanAttribute}) + readonly!: boolean + + @ViewChild('mobilePagesContainer') + mobilePagesContainer: ElementRef; + + pagesForm = this.fb.group({ + pages: this.fb.array([]) + }); + + maxIconNameBlockWidth = 256; + + private hideItemsSubject = new Subject(); + hideItems$ = this.hideItemsSubject.asObservable(); + + private propagateChange = (_val: any) => {}; + + constructor(private fb: FormBuilder, + private breakpointObserver: BreakpointObserver, + private dialog: MatDialog, + ) { + + this.pagesForm.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe( + () => this.updateModel() + ); + + this.breakpointObserver.observe([MediaBreakpoints.xs, MediaBreakpoints['gt-xs'], MediaBreakpoints['gt-sm']]).pipe( + takeUntilDestroyed() + ).subscribe(() => { + this.computeMaxIconNameBlockWidth(); + }); + this.computeMaxIconNameBlockWidth(); + } + + registerOnChange(fn: any) { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any) { + } + + setDisabledState(isDisabled: boolean) { + if (isDisabled) { + this.pagesForm.disable({emitEvent: false}); + } else { + this.pagesForm.enable({emitEvent: false}); + } + } + + writeValue(layout: MobileLayoutConfig) { + const processLayout = this.prepareMobilePages(layout); + this.pagesForm.setControl('pages', this.prepareMobilePagesFormArray(processLayout)); + } + + validate(_c: FormControl) { + if (!this.pagesForm.valid) { + return { + invalidLayoutForm: true + }; + } + return null; + } + + hideAll() { + this.hideItemsSubject.next(); + } + + resetToDefault() { + if (!isDefaultMobilePagesConfig(this.pagesForm.value.pages as MobilePage[])) { + const processLayout = this.prepareMobilePages(null); + this.pagesForm.setControl('pages', this.prepareMobilePagesFormArray(processLayout)); + } + } + + get dragEnabled(): boolean { + return !this.readonly && this.visibleMobilePagesControls().length > 1; + } + + visibleMobilePagesControls(): Array { + return this.pagesFormArray().controls; + } + + mobileItemDrop(event: CdkDragDrop) { + const menuItemsArray = this.pagesFormArray(); + const menuItem = this.visibleMobilePagesControls()[event.previousIndex]; + const previousIndex = this.actualMobilePageIndex(event.previousIndex); + const currentIndex = this.actualMobilePageIndex(event.currentIndex); + menuItemsArray.removeAt(previousIndex); + menuItemsArray.insert(currentIndex, menuItem); + this.pagesForm.markAsDirty(); + } + + trackByMenuItem(_index: number, menuItemControl: AbstractControl): any { + return menuItemControl; + } + + addCustomMobilePage(index?: number) { + this.dialog.open(AddMobilePageDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe((menuItem) => { + if (menuItem) { + const menuItemsArray = this.pagesFormArray(); + const menuItemControl = this.fb.control(menuItem, []); + if (isDefined(index)) { + const insertIndex = this.actualMobilePageIndex(index) + 1; + menuItemsArray.insert(insertIndex, menuItemControl); + } else { + menuItemsArray.push(menuItemControl); + setTimeout(() => { + this.mobilePagesContainer.nativeElement.scrollTop = this.mobilePagesContainer.nativeElement.scrollHeight; + }, 0); + } + this.pagesForm.markAsDirty(); + } + }); + } + + isCustom(menuItemControl: AbstractControl): boolean { + return menuItemControl.value.type !== MobilePageType.DEFAULT; + } + + removeCustomPage(index: number) { + this.pagesFormArray().removeAt(this.actualMobilePageIndex(index)); + this.pagesForm.markAsDirty(); + } + + showMenuDivider(index: number): boolean { + return mobileMenuDividers.has(index); + } + + getDividerLabel(index: number): string { + return mobileMenuDividers.get(index); + } + + private updateModel() { + if (isDefaultMobilePagesConfig(this.pagesForm.value.pages as MobilePage[])) { + this.propagateChange({pages: []}); + } else { + this.propagateChange(this.pagesForm.value); + } + } + + private prepareMobilePages(layout: MobileLayoutConfig) { + if (!layout?.pages?.length) { + return getDefaultMobileMenuItem(); + } + return layout.pages; + } + + private prepareMobilePagesFormArray(items: MobilePage[]): FormArray { + const menuItemsControls: Array = []; + items.forEach((item) => { + menuItemsControls.push(this.fb.control(deepClone(item))); + }); + return this.fb.array(menuItemsControls); + } + + private computeMaxIconNameBlockWidth() { + if (this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm'])) { + this.maxIconNameBlockWidth = 256; + } else if (this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs'])) { + this.maxIconNameBlockWidth = 200; + } else { + this.maxIconNameBlockWidth = 0; + } + } + + private pagesFormArray(): FormArray { + return this.pagesForm.get('pages') as FormArray; + } + + private actualMobilePageIndex(index: number): number { + const menuItem = this.visibleMobilePagesControls()[index]; + return this.pagesFormArray().controls.indexOf(menuItem); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html new file mode 100644 index 0000000000..873eb68a13 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html @@ -0,0 +1,75 @@ + +
+
+
+
+ +
+ + + +
{{ itemName }}
+
+
+
+ + + + warning + + + + + +
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.scss new file mode 100644 index 0000000000..ebee7308a3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.scss @@ -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. + */ +@import "../scss/constants"; +@import '../scss/mixins'; + +.tb-mobile-page-item-row-component-container { + display: flex; + flex-direction: column; + gap: 0; +} + +.tb-mobile-page-item-row-component { + display: flex; + gap: 8px; + align-items: center; + .mat-mdc-icon-button.mat-mdc-button-base { + &:not(.tb-drag-handle) { + @media #{$mat-xs} { + @include tb-mat-icon-button-size(24); + } + @media #{$mat-sm} { + @include tb-mat-icon-button-size(32); + } + } + &:not(:disabled) { + .mat-icon { + color: rgba(0, 0, 0, 0.54); + } + } + &:disabled { + .mat-icon { + color: rgba(0, 0, 0, 0.12); + } + } + } + .tb-form-row { + flex: 1; + min-width: 0; + .tb-mobile-page-item-start-block { + display: flex; + align-items: center; + gap: 0; + min-width: 0; + } + .tb-mobile-page-item-icon-name-block { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; + } + .tb-mobile-page-item-input-block { + display: flex; + gap: 8px; + flex: 1; + } + } +} + +.tb-mobile-page-item-info { + .mdc-text-field:not(.mdc-text-field--disabled) { + input.mdc-text-field__input { + cursor: default; + &::placeholder { + color: $tb-primary-color; + } + &:-ms-input-placeholder { + color: $tb-primary-color; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts new file mode 100644 index 0000000000..eb9f7a2d63 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts @@ -0,0 +1,323 @@ +/// +/// 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. +/// + +import { + booleanAttribute, + ChangeDetectorRef, + Component, + DestroyRef, + EventEmitter, + forwardRef, + inject, + Input, + numberAttribute, + OnInit, + Output, + Renderer2, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { Observable } from 'rxjs'; +import { + CustomMobilePage, + DefaultMobilePage, + defaultMobilePageMap, + hideDefaultMenuItems, + MobilePageType, + mobilePageTypeTranslations +} from '@shared/models/mobile-app.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatButton } from '@angular/material/button'; +import { deepClone } from '@core/utils'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { CustomMobilePagePanelComponent } from '@home/pages/mobile/bundes/layout/custom-mobile-page-panel.component'; +import { DefaultMobilePagePanelComponent } from '@home/pages/mobile/bundes/layout/default-mobile-page-panel.component'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-mobile-menu-item-row', + templateUrl: './mobile-page-item-row.component.html', + styleUrls: ['./mobile-page-item-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MobilePageItemRowComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MobilePageItemRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class MobilePageItemRowComponent implements ControlValueAccessor, OnInit, Validator { + + @Input({transform: booleanAttribute}) + disabled: boolean; + + @Input({transform: numberAttribute}) + maxIconNameBlockWidth = 256; + + @Input() + hideItems: Observable; + + @Output() + pageRemoved = new EventEmitter(); + + iconNameBlockWidth = '256px'; + itemInfo: string; + + isDefaultMenuItem = false; + isCustomMenuItem = false; + + isCleanupEnabled = false; + + mobilePageRowForm = this.fb.group({ + visible: [true, []], + icon: ['', []], + label: ['', []], + type: [MobilePageType.DEFAULT] + }); + + private propagateChange = (_val: any) => {}; + private destroyRef = inject(DestroyRef); + + private defaultMobilePages: Omit; + private defaultItemName: string; + + private modelValue: DefaultMobilePage | CustomMobilePage; + + constructor(private fb: FormBuilder, + private cd: ChangeDetectorRef, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private translate: TranslateService) { + this.mobilePageRowForm.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe( + () => this.updateModel() + ); + } + + ngOnInit() { + this.updateIconNameBlockWidth(); + if (this.hideItems) { + this.hideItems.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + if (!this.disabled && this.modelValue.visible) { + this.mobilePageRowForm.patchValue( + {visible: false}, {emitEvent: true} + ); + } + }); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean) { + this.disabled = isDisabled; + if (isDisabled) { + this.mobilePageRowForm.disable({emitEvent: false}); + } else { + this.mobilePageRowForm.enable({emitEvent: false}); + } + } + + validate(): ValidationErrors | null { + if (!this.mobilePageRowForm.valid) { + return { + invalidMobileMenuItem: true + }; + } + return null; + } + + writeValue(value: DefaultMobilePage | CustomMobilePage) { + this.modelValue = value; + this.mobilePageRowForm.patchValue( + { + visible: value.visible, + icon: value.icon, + label: value.label + }, {emitEvent: false} + ); + if (this.modelValue.type === MobilePageType.DEFAULT) { + this.isDefaultMenuItem = true; + const defaultPage = this.modelValue as DefaultMobilePage; + if (defaultMobilePageMap.has(defaultPage.id)) { + this.defaultMobilePages = defaultMobilePageMap.get(defaultPage.id); + this.defaultItemName = this.defaultMobilePages.label; + if (!value.icon) { + this.mobilePageRowForm.patchValue({ + icon: this.defaultMobilePages.icon + }, {emitEvent: false}); + } + } + } else { + this.isCustomMenuItem = true; + this.mobilePageRowForm.get('label').setValidators([Validators.required]); + this.mobilePageRowForm.get('label').updateValueAndValidity({emitEvent: false}); + } + this.updateCleanupState(); + this.updateItemInfo(); + this.cd.markForCheck(); + } + + cleanup() { + const visible = !hideDefaultMenuItems.includes((this.modelValue as DefaultMobilePage).id) + this.mobilePageRowForm.patchValue( + { + visible: visible, + icon: this.defaultMobilePages.icon, + label: null + }, {emitEvent: false} + ); + this.modelValue.visible = visible; + delete this.modelValue.icon; + delete this.modelValue.label; + this.updateModel(); + } + + delete() { + this.pageRemoved.emit(); + } + + edit($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + disabled: this.disabled, + pageItem: deepClone(this.modelValue) + }; + if (this.isDefaultMenuItem) { + const defaultMobilePagePanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, DefaultMobilePagePanelComponent, ['right', 'bottom', 'top'], true, null, + ctx, + {}, + {}, {}, false, () => {}, {padding: '16px 24px'}); + defaultMobilePagePanelPopover.tbComponentRef.instance.popover = defaultMobilePagePanelPopover; + defaultMobilePagePanelPopover.tbComponentRef.instance.defaultMobilePageApplied.subscribe((menuItem) => { + defaultMobilePagePanelPopover.hide(); + this.afterPageEdit(menuItem); + }); + } else { + const customMobilePagePanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, CustomMobilePagePanelComponent, ['right', 'bottom', 'top'], true, null, + ctx, + {}, + {}, {}, false, () => {}, {padding: '16px 24px'}); + customMobilePagePanelPopover.tbComponentRef.instance.popover = customMobilePagePanelPopover; + customMobilePagePanelPopover.tbComponentRef.instance.customMobilePageApplied.subscribe((page) => { + customMobilePagePanelPopover.hide(); + this.afterPageEdit(page); + }); + } + } + } + + get itemName(): string { + return this.isDefaultMenuItem ? this.defaultItemName : this.mobilePageRowForm.get('label').value; + } + + get itemNamePlaceholder(): string { + return this.isDefaultMenuItem ? this.defaultItemName : ''; + } + + private updateIconNameBlockWidth() { + if (this.maxIconNameBlockWidth) { + this.iconNameBlockWidth = `${this.maxIconNameBlockWidth}px`; + } else { + this.iconNameBlockWidth = '100%'; + } + } + + private updateModel() { + this.modelValue.visible = this.mobilePageRowForm.get('visible').value; + const label = this.mobilePageRowForm.get('label').value; + if (label) { + this.modelValue.label = label; + } else { + delete this.modelValue.label; + } + let icon = this.mobilePageRowForm.get('icon').value; + if (this.isDefaultMenuItem) { + if (this.defaultMobilePages.icon === icon) { + icon = null; + } + } + if (icon) { + this.modelValue.icon = icon; + } else { + delete this.modelValue.icon; + } + this.updateCleanupState(); + this.updateItemInfo(); + this.propagateChange(this.modelValue); + } + + private updateCleanupState() { + if (this.isDefaultMenuItem) { + this.isCleanupEnabled = + (hideDefaultMenuItems.includes((this.modelValue as DefaultMobilePage).id) ? this.modelValue.visible : !this.modelValue.visible) || + !!this.modelValue.label || + !!this.modelValue.icon; + } + } + + private updateItemInfo() { + if (this.isCustomMenuItem) { + this.itemInfo = this.translate.instant(mobilePageTypeTranslations.get(this.modelValue.type)); + } else { + this.itemInfo = ''; + } + } + + private afterPageEdit(page: DefaultMobilePage | CustomMobilePage) { + this.mobilePageRowForm.patchValue({ + visible: page.visible, + icon: page.icon || (this.isDefaultMenuItem ? this.defaultMobilePages.icon : null), + label: page.label + }, {emitEvent: false}); + if (this.isCustomMenuItem) { + this.modelValue = page; + } + this.updateModel(); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html new file mode 100644 index 0000000000..9d7fb0ca4e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html @@ -0,0 +1,109 @@ + + +

{{ dialogTitle | translate }}

+ +
+ +
+ + +
+
+ + + check + + + {{ 'mobile.basic-settings' | translate }} +
+ + mobile.title + + + + {{ 'mobile.title-required' | translate }} + + + + + + + + mobile.description + + +
+
+ + {{ 'admin.oauth2.oauth2' | translate }} +
+ + {{ 'mobile.enable-oauth' | translate }} + + + + +
+
+ + {{ 'mobile.layout' | translate }} +
+ + +
+
+
+
+
+ + + +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.scss new file mode 100644 index 0000000000..3649f88d5c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.scss @@ -0,0 +1,73 @@ +/** + * 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. + */ +@import "../../../../../../scss/constants"; + +:host { + width: 850px; + height: 100%; + max-width: 100%; + max-height: 100vh; + display: grid; + grid-template-rows: min-content 4px minmax(auto, 1fr) min-content; + + .mat-mdc-slide-toggle { + margin-bottom: 16px; + } + + form.layout-form { + display: block; + height: 100%; + max-height: 100%; + } + + ::ng-deep { + + .mat-mdc-dialog-content { + display: flex; + flex-direction: column; + height: 100%; + padding: 0 !important; + color: rgba(0, 0, 0, 0.87); + + .mat-stepper-horizontal { + display: flex; + height: 100%; + overflow: hidden; + + .mat-horizontal-stepper-wrapper { + flex: 1 1 100%; + max-width: 100%; + } + + .mat-horizontal-content-container { + height: 700px; + max-height: 100%; + width: 100%;; + overflow-y: auto; + scrollbar-gutter: stable; + @media #{$mat-gt-sm} { + min-width: 500px; + } + .mat-horizontal-stepper-content:not(.mat-horizontal-stepper-content-inactive):nth-child(3) { + height: 100%; + max-height: 100%; + overflow: hidden; + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts new file mode 100644 index 0000000000..8e07bcbc30 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts @@ -0,0 +1,226 @@ +/// +/// 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. +/// + +import { Component, Inject, ViewChild } from '@angular/core'; +import { MobileApp, MobileAppBundle, MobileAppBundleInfo } from '@shared/models/mobile-app.models'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { map } from 'rxjs/operators'; +import { forkJoin, Observable } from 'rxjs'; +import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { FormBuilder, Validators } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { PlatformType } from '@shared/models/oauth2.models'; +import { MatStepper } from '@angular/material/stepper'; +import { + MobileAppDialogComponent, + MobileAppDialogData +} from '@home/pages/mobile/applications/mobile-app-dialog.component'; +import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MobileAppService } from '@core/http/mobile-app.service'; +import { deepClone, deepTrim } from '@core/utils'; + +export interface MobileBundleDialogData { + bundle?: MobileAppBundleInfo; + isAdd?: boolean; +} + +@Component({ + selector: 'tb-mobile-bundle-dialog', + templateUrl: './mobile-bundle-dialog.component.html', + styleUrls: ['./mobile-bundle-dialog.component.scss'] +}) +export class MobileBundleDialogComponent extends DialogComponent { + + @ViewChild('addMobileBundle', {static: true}) addMobileBundle: MatStepper; + + readonly entityType = EntityType; + + selectedIndex = 0; + + dialogTitle = 'mobile.edit-bundle'; + + stepperOrientation: Observable; + + platformType = PlatformType; + + bundlesForms = this.fb.group({ + title: ['', Validators.required], + androidAppId: [null], + iosAppId: [null], + description: [''], + }); + + oauthForms = this.fb.group({ + oauth2Enabled: [true], + oauth2ClientIds: [null] + }); + + layoutForms = this.fb.group({ + layoutConfig: [null] + }); + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: MobileBundleDialogData, + private breakpointObserver: BreakpointObserver, + private fb: FormBuilder, + private dialog: MatDialog, + private mobileAppService: MobileAppService) { + super(store, router, dialogRef); + + if (this.data.isAdd) { + this.dialogTitle = 'mobile.add-bundle' + } + + this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs']) + .pipe(map(({matches}) => matches ? 'horizontal' : 'vertical')); + + this.oauthForms.get('oauth2Enabled').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(value => { + if (value) { + this.oauthForms.get('oauth2ClientIds').enable({emitEvent: false}); + } else { + this.oauthForms.get('oauth2ClientIds').disable({emitEvent: false}); + } + }) + + if (!this.data.isAdd && this.data.bundle) { + this.bundlesForms.patchValue(this.data.bundle, {emitEvent: false}); + this.oauthForms.get('oauth2Enabled').setValue(this.data.bundle.oauth2Enabled, {onlySelf: true}); + this.oauthForms.get('oauth2ClientIds') + .setValue(deepClone(this.data.bundle.oauth2ClientInfos.map(item => item.id.id)), {emitEvent: false}); + this.layoutForms.patchValue(this.data.bundle, {emitEvent: false}); + } + } + + cancel(): void { + this.dialogRef.close(null); + } + + backStep() { + this.addMobileBundle.previous(); + } + + nextStep() { + if (this.selectedIndex >= this.maxStepperIndex) { + this.add(); + } else { + this.addMobileBundle.next(); + } + } + + nextStepLabel(): string { + if (this.selectedIndex >= this.maxStepperIndex) { + return this.data.isAdd ? 'action.add' : 'action.save'; + } + return 'action.next'; + } + + changeStep($event: StepperSelectionEvent) { + this.selectedIndex = $event.selectedIndex; + if ($event.previouslySelectedIndex > $event.selectedIndex) { + $event.previouslySelectedStep.interacted = false; + } + } + + createApplication(formControl: string, platformType: PlatformType) { + this.dialog.open(MobileAppDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + platformType + } + }).afterClosed() + .subscribe((app) => { + if (app) { + this.bundlesForms.get(formControl).patchValue(app.id); + this.bundlesForms.get(formControl).markAsDirty(); + } + }); + } + + createClient($event: Event) { + if ($event) { + $event.stopPropagation(); + $event.preventDefault(); + } + this.dialog.open(ClientDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: {} + }).afterClosed() + .subscribe((client) => { + if (client) { + const formValue = this.oauthForms.get('oauth2ClientIds').value ? + [...this.oauthForms.get('oauth2ClientIds').value] : []; + formValue.push(client.id.id); + this.oauthForms.get('oauth2ClientIds').patchValue(formValue); + this.oauthForms.get('oauth2ClientIds').markAsDirty(); + } + }); + } + + private get maxStepperIndex(): number { + return this.addMobileBundle?._steps?.length - 1; + } + + private add(): void { + if (this.allValid()) { + let task = { + mobileBundle: this.mobileAppService.saveMobileAppBundle(this.mobileAppBundleFormValue, this.oauthForms.value.oauth2ClientIds), + oath2Clients: null + } + if (this.data.isAdd) { + delete task.oath2Clients; + } else { + const mobileBundle: MobileAppBundle = {...this.data.bundle, ...this.mobileAppBundleFormValue}; + task.mobileBundle = this.mobileAppService.saveMobileAppBundle(mobileBundle); + task.oath2Clients = this.mobileAppService.updateOauth2Clients(mobileBundle.id.id, this.oauthForms.get('oauth2ClientIds').value); + } + forkJoin(task).subscribe( + (res) => this.dialogRef.close(res.mobileBundle) + ); + } + } + + private allValid(): boolean { + return !this.addMobileBundle.steps.find((item, index) => { + if (item.stepControl?.invalid) { + item.interacted = true; + this.addMobileBundle.selectedIndex = index; + return true; + } else { + return false; + } + }); + } + + private get mobileAppBundleFormValue(): MobileAppBundle { + const formValue = deepTrim(this.bundlesForms.value) as MobileAppBundle; + formValue.layoutConfig = deepTrim(this.layoutForms.value.layoutConfig); + formValue.oauth2Enabled = this.oauthForms.value.oauth2Enabled; + return formValue; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts new file mode 100644 index 0000000000..735b2b877b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts @@ -0,0 +1,142 @@ +/// +/// 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. +/// + +import { Injectable } from '@angular/core'; +import { + CellActionDescriptor, + checkBoxCell, + DateEntityTableColumn, + EntityChipsEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { MobileAppBundleInfo } from '@shared/models/mobile-app.models'; +import { ActivatedRouteSnapshot } from '@angular/router'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { Direction } from '@shared/models/page/sort-order'; +import { MobileBundleTableHeaderComponent } from '@home/pages/mobile/bundes/mobile-bundle-table-header.component'; +import { DatePipe } from '@angular/common'; +import { MobileAppService } from '@core/http/mobile-app.service'; +import { map } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { MatDialog } from '@angular/material/dialog'; +import { + MobileBundleDialogComponent, + MobileBundleDialogData +} from '@home/pages/mobile/bundes/mobile-bundle-dialog.component'; +import { NotificationTemplate } from '@shared/models/notification.models'; + +@Injectable() +export class MobileBundleTableConfigResolver { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor( + private datePipe: DatePipe, + private mobileAppService: MobileAppService, + private translate : TranslateService, + private dialog: MatDialog, + ) { + this.config.selectionEnabled = false; + this.config.entityType = EntityType.MOBILE_APP_BUNDLE; + this.config.addEnabled = false; + this.config.rowPointer = true; + this.config.detailsPanelEnabled = false; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.MOBILE_APP_BUNDLE); + this.config.entityResources = entityTypeResources.get(EntityType.MOBILE_APP_BUNDLE); + this.config.headerComponent = MobileBundleTableHeaderComponent; + this.config.onEntityAction = action => this.onBundleAction(action); + this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'}; + this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('title', 'mobile.title', '25%'), + new EntityChipsEntityTableColumn('oauth2ClientInfos', 'mobile.oauth-clients', '35%'), + new EntityChipsEntityTableColumn('androidPkg', 'mobile.android-app', '20%'), + new EntityChipsEntityTableColumn('iosPkg', 'mobile.ios-app', '20%'), + new EntityTableColumn('oauth2Enabled', 'mobile.enable-oauth', '140px', + entity => checkBoxCell(entity.oauth2Enabled)) + ) + + this.config.deleteEnabled = bundle => !(bundle.iosAppId || bundle.androidAppId); + this.config.deleteEntityTitle = (bundle) => this.translate.instant('mobile.delete-applications-bundle-title', {bundleName: bundle.name}); + this.config.deleteEntityContent = () => this.translate.instant('mobile.delete-applications-bundle-text'); + this.config.deleteEntity = id => this.mobileAppService.deleteMobileAppBundle(id.id); + + this.config.entitiesFetchFunction = pageLink => this.mobileAppService.getTenantMobileAppBundleInfos(pageLink).pipe( + map(bundles => { + bundles.data.map(data => { + if (data.androidPkgName) { + data.androidPkg = { + id: data.androidAppId, + name: data.androidPkgName + } + } + if (data.iosPkgName) { + data.iosPkg = { + id: data.iosAppId, + name: data.iosPkgName + } + } + }) + return bundles; + }) + ); + + this.config.handleRowClick = ($event, bundle) => { + $event?.stopPropagation(); + this.mobileAppService.getMobileAppBundleInfoById(bundle.id.id).subscribe(appBundleInfo => { + this.editBundle($event, appBundleInfo); + }) + return true; + }; + } + + resolve(_route: ActivatedRouteSnapshot): EntityTableConfig { + return this.config; + } + + private editBundle($event: Event, bundle: MobileAppBundleInfo, isAdd = false) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(MobileBundleDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + bundle + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.updateData(); + } + }); + } + + private onBundleAction(action: EntityAction): boolean { + switch (action.action) { + case 'add': + this.editBundle(action.event, action.entity, true); + return true; + } + return false; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.html new file mode 100644 index 0000000000..0f4f435a15 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.html @@ -0,0 +1,28 @@ + +
+
+
mobile.bundles
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.scss new file mode 100644 index 0000000000..06f14e4712 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.scss @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host{ + width: 100000px; + + .tb-entity-title-container { + display: flex; + align-items: center; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.ts new file mode 100644 index 0000000000..7d97c82da0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.ts @@ -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. +/// + +import { Component } from '@angular/core'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { MobileAppBundleInfo } from '@shared/models/mobile-app.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-mobile-bundle-table-header', + templateUrl: './mobile-bundle-table-header.component.html', + styleUrls: ['./mobile-bundle-table-header.component.scss'] +}) +export class MobileBundleTableHeaderComponent extends EntityTableHeaderComponent { + + constructor(protected store: Store) { + super(store); + } + + createBundle($event: Event) { + this.entitiesTableConfig.onEntityAction({event: $event, action: 'add', entity: null}); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts index 4240bb7b78..6665c716b9 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts @@ -23,6 +23,7 @@ import { MobileAppTableConfigResolver } from '@home/pages/mobile/applications/mo import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { applicationsRoutes } from '@home/pages/mobile/applications/applications-routing.module'; +import { bundlesRoutes } from '@home/pages/mobile/bundes/bundles-routing.module'; const routes: Routes = [ { @@ -44,6 +45,7 @@ const routes: Routes = [ } }, ...applicationsRoutes, + ...bundlesRoutes, { path: 'mobile-app', component: MobileAppSettingsComponent, diff --git a/ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts index 477fe4ae18..9c47d507c3 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts @@ -19,14 +19,16 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { HomeComponentsModule } from '@home/components/home-components.module'; import { MobileRoutingModule } from '@home/pages/mobile/mobile-routing.module'; -import { ApplicationModule } from '@home/pages/mobile/applications/applications.module'; +import { MobileApplicationModule } from '@home/pages/mobile/applications/applications.module'; +import { MobileBundlesModule } from '@home/pages/mobile/bundes/bundles.module'; @NgModule({ imports: [ CommonModule, SharedModule, HomeComponentsModule, - ApplicationModule, + MobileApplicationModule, + MobileBundlesModule, MobileRoutingModule, ] }) diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html index 2ca193468c..c860835026 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html @@ -33,6 +33,12 @@ (click)="clear()"> close + diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index b55f01a7ae..7589f41aa2 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -131,9 +131,16 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit @coerceBoolean() disabled: boolean; + @Input() + @coerceBoolean() + allowCreateNew: boolean; + @Output() entityChanged = new EventEmitter>(); + @Output() + createNew = new EventEmitter(); + @ViewChild('entityInput', {static: true}) entityInput: ElementRef; get requiredErrorText(): string { @@ -266,6 +273,12 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit this.entityRequiredText = 'queue-statistics.queue-statistics-required'; this.notFoundEntities = 'queue-statistics.no-queue-statistics-text'; break; + case EntityType.MOBILE_APP: + this.entityText = 'mobile.applications'; + this.noEntitiesMatchingText = 'mobile.no-application-matching'; + this.entityRequiredText = 'mobile.application-required'; + this.notFoundEntities = 'mobile.no-application-text'; + break; case AliasEntityType.CURRENT_CUSTOMER: this.entityText = 'customer.default-customer'; this.noEntitiesMatchingText = 'customer.no-customers-matching'; @@ -417,4 +430,9 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit } return entityType; } + + createNewEntity($event: Event) { + $event.stopPropagation(); + this.createNew.emit(); + } } diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-list.component.html index bd539911c8..6b369c77e6 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + {{ labelText }} ([ [EntityType.WIDGET_TYPE, '/resources/widgets-library/widget-types/details'], [EntityType.OAUTH2_CLIENT, '/security-settings/oauth2/clients/details'], [EntityType.DOMAIN, '/security-settings/oauth2/clients/details'], - [EntityType.MOBILE_APP, '/security-settings/oauth2/clients/details'] + [EntityType.MOBILE_APP, '/mobile-center/applications'] ]); export interface EntitySubtype { diff --git a/ui-ngx/src/app/shared/models/mobile-app.models.ts b/ui-ngx/src/app/shared/models/mobile-app.models.ts index 55579d2e8c..ce07a1ef71 100644 --- a/ui-ngx/src/app/shared/models/mobile-app.models.ts +++ b/ui-ngx/src/app/shared/models/mobile-app.models.ts @@ -19,6 +19,7 @@ import { BaseData } from '@shared/models/base-data'; import { MobileAppId } from '@shared/models/id/mobile-app-id'; import { OAuth2ClientInfo, PlatformType } from '@shared/models/oauth2.models'; import { MobileAppBundleId } from '@shared/models/id/mobile-app-bundle-id'; +import { deepClone, isNotEmptyStr } from '@core/utils'; export interface QrCodeSettings extends HasTenantId { useDefaultApp: boolean; @@ -119,20 +120,43 @@ enum MobileMenuPath { DASHBOARD = 'DASHBOARD', AUDIT_LOGS = 'AUDIT_LOGS', CUSTOMERS = 'CUSTOMERS', - CUSTOMER = 'CUSTOMER', - NOTIFICATION = 'NOTIFICATION', - CUSTOM = 'CUSTOM' + NOTIFICATIONS = 'NOTIFICATIONS' } -export interface MobileMenuItem { - label: string; - icon: string; - path: MobileMenuPath; - id: string; +export enum MobilePageType { + DEFAULT = 'DEFAULT', + CUSTOM = 'CUSTOM', + DASHBOARD = 'DASHBOARD', + WEB_VIEW = 'WEB_VIEW', +} + +export const mobilePageTypeTranslations = new Map( + [ + [MobilePageType.CUSTOM, 'mobile.pages-types.custom'], + [MobilePageType.DASHBOARD, 'mobile.pages-types.dashboard'], + [MobilePageType.WEB_VIEW, 'mobile.pages-types.web-view'], + ] +); + +export interface MobilePage { + label?: string; + icon?: string; + type: MobilePageType; + visible: boolean; +} + +export interface DefaultMobilePage extends MobilePage { + id: MobileMenuPath; +} + +export interface CustomMobilePage extends MobilePage { + dashboardId?: string; + url?: string; + path?: string; } export interface MobileLayoutConfig { - items: MobileMenuItem[]; + pages: MobilePage[]; } export interface MobileAppBundle extends Omit, 'label'>, HasTenantId { @@ -147,5 +171,152 @@ export interface MobileAppBundle extends Omit, 'labe export interface MobileAppBundleInfo extends MobileAppBundle { androidPkgName: string; iosPkgName: string; + androidPkg?: { + name: string; + id: MobileAppId + }; + iosPkg?: { + name: string; + id: MobileAppId + } oauth2ClientInfos?: Array; + qrCodeEnabled: boolean; } + +const defaultMobileMenu = [ + MobileMenuPath.HOME, + MobileMenuPath.ALARMS, + MobileMenuPath.DEVICES, + MobileMenuPath.CUSTOMERS, + MobileMenuPath.ASSETS, + MobileMenuPath.AUDIT_LOGS, + MobileMenuPath.NOTIFICATIONS, + MobileMenuPath.DEVICE_LIST, + MobileMenuPath.DASHBOARDS +]; + +export const hideDefaultMenuItems = [ + MobileMenuPath.DEVICE_LIST, + MobileMenuPath.DASHBOARDS +]; + +export const getDefaultMobileMenuItem = (): DefaultMobilePage[] => { + return deepClone(defaultMobileMenu).map(item => ({ + visible: !hideDefaultMenuItems.includes(item), + type: MobilePageType.DEFAULT, + id: item + })) +} + +export const isDefaultMobileMenuItem = (item: MobilePage): item is DefaultMobilePage => { + const path = (item as DefaultMobilePage).id; + return isNotEmptyStr(path) && defaultMobilePageMap.has(path); +}; + + +const mobilePageEqualToDefault = (item: MobilePage, defaultMobilePage: DefaultMobilePage): boolean => { + if (isDefaultMobileMenuItem(item) && (hideDefaultMenuItems.includes(item.id) ? !item.visible : item.visible)) { + return !(item.id !== defaultMobilePage.id || !!item.label || !!item.icon); + } else { + return false; + } +}; + +export const isDefaultMobilePagesConfig = (items: MobilePage[]): boolean => { + const defaultMenus = getDefaultMobileMenuItem(); + if (!items?.length && !defaultMenus?.length) { + return true; + } else if (items.length !== defaultMenus.length) { + return false; + } else { + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const defaultMenuItem = defaultMenus[i]; + if (!mobilePageEqualToDefault(item, defaultMenuItem)) { + return false; + } + } + return true; + } +}; + +export const mobileMenuDividers = new Map([ + [2, 'mobile.mobile-599'], + [4, 'mobile.tablet-959'], + [6, 'mobile.max-element-number'], +]); + +export const defaultMobilePageMap = new Map>([ + [ + MobileMenuPath.HOME, + { + id: MobileMenuPath.HOME, + icon: 'home', + label: 'Home' + } + ], + [ + MobileMenuPath.ALARMS, + { + id: MobileMenuPath.ALARMS, + icon: 'notifications', + label: 'Alarms' + } + ], + [ + MobileMenuPath.DEVICES, + { + id: MobileMenuPath.DEVICES, + icon: 'devices_other', + label: 'Devices' + } + ], + [ + MobileMenuPath.CUSTOMERS, + { + id: MobileMenuPath.CUSTOMERS, + icon: 'supervisor_account', + label: 'Customers' + } + ], + [ + MobileMenuPath.ASSETS, + { + id: MobileMenuPath.ASSETS, + icon: 'domain', + label: 'Assets' + } + ], + [ + MobileMenuPath.DEVICE_LIST, + { + id: MobileMenuPath.DEVICE_LIST, + icon: 'devices', + label: 'Device list' + } + ], + [ + MobileMenuPath.DASHBOARDS, + { + id: MobileMenuPath.DASHBOARDS, + icon: 'dashboard', + label: 'Dashboards' + } + ], + [ + MobileMenuPath.AUDIT_LOGS, + { + id: MobileMenuPath.AUDIT_LOGS, + icon: 'track_changes', + label: 'Audit logs' + } + ], + [ + MobileMenuPath.NOTIFICATIONS, + { + id: MobileMenuPath.NOTIFICATIONS, + icon: 'notifications_active', + label: 'Notification' + } + ] +]) diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d600843dd6..1e6e58487e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2384,7 +2384,10 @@ "list-of-domains": "{ count, plural, =1 {One Domain} other {List of # Domains} }", "type-mobile-app": "Mobile application", "type-mobile-apps": "Mobile applications", - "list-of-mobile-apps": "{ count, plural, =1 {One Mobile application} other {List of # Mobile applications} }" + "list-of-mobile-apps": "{ count, plural, =1 {One Mobile application} other {List of # Mobile applications} }", + "type-mobile-app-bundle": "Mobile bundle", + "type-mobile-app-bundles": "Mobile bundles", + "list-of-mobile-app-bundles": "{ count, plural, =1 {One mobile bundle} other {List of # mobile bundles} }" }, "entity-field": { "created-time": "Created time", @@ -4077,6 +4080,8 @@ "copy-sha256-certificate-fingerprints": "Copy SHA256 certificate fingerprints", "delete-applications-text": "Be careful, after the confirmation the mobile application and all related data will become unrecoverable.", "delete-applications-title": "Are you sure you want to delete the mobile application '{{applicationName}}'?", + "delete-applications-bundle-text": "Be careful, after the confirmation the mobile bundle and all related data will become unrecoverable.", + "delete-applications-bundle-title": "Are you sure you want to delete the mobile bundle '{{bundleName}}'?", "generate-application-secret": "Generate application secret", "google-play-link": "Google Play link", "google-play-link-required": "Google Play link is required", @@ -4087,9 +4092,11 @@ "mobile-package-max-length": "Application package should be less than 256", "mobile-package-required": "Application package is required.", "mobile-package-spaces": "Application package should not contain spaces", - "no=application": "No applications found", + "no-application": "No applications found", + "no-bundles": "No bundles found", "platform-type": "Platform type", "search-application": "Search applications", + "search-bundles": "Search bundles", "set": "Set", "sha256-certificate-fingerprints": "SHA256 certificate fingerprints", "sha256-certificate-fingerprints-required": "SHA256 certificate fingerprints is required", @@ -4103,7 +4110,49 @@ "store-information": "Store information", "version-information": "Version information", "min-version-release-notes": "Min version release notes", - "latest-version-release-notes": "Latest version release notes" + "latest-version-release-notes": "Latest version release notes", + "bundles": "Bundles", + "add-bundle": "Add bundle", + "title": "Title", + "title-required": "Title is required", + "oauth-clients": "OAuth 2.0 clients", + "android-app": "Android App", + "android-application": "Android Application", + "ios-app": "iOS App", + "ios-application": "iOS Application", + "enable-oauth" : "Enable OAuth 2.0", + "enable-self-registration": "Enable Self registration", + "edit-bundle": "Edit bundle", + "description": "Description", + "basic-settings": "Basic settings", + "no-application-matching": "No application matching '{{entity}}' were found.", + "application-required": "Application is required.", + "no-application-text": "No applications found", + "layout": "Layout", + "pages": "Pages", + "hide-all-pages": "Hide all pages", + "reset-to-default-pages": "Reset to default pages", + "add-specific-page": "Add specific page", + "visible": "Visible", + "hidden": "Hidden", + "reset-to-page-default": "Reset page back to default", + "mobile-599": "Mobile (max 599px)", + "tablet-959": "Tablet (max 959px)", + "max-element-number": "Max elements number", + "page-name": "Page name", + "page-nam-required": "Page name is required.", + "page-type": "Page type", + "pages-types": { + "dashboard": "Dashboard", + "web-view": "Web view", + "custom": "Custom" + }, + "url": "URL", + "path": "Path", + "custom-page": "Custom page", + "edit-page": "Edit page", + "edit-custom-page": "Edit custom page", + "delete-page": "Delete page" }, "notification": { "action-button": "Action button", From 2dafb69c5ebd2d0e1dcb729063ea4ce4ca13dbec Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 22 Oct 2024 15:40:33 +0300 Subject: [PATCH 18/48] UI: Move mobile-app-settings.component --- .../home/pages/admin/admin-routing.module.ts | 13 ------------- .../app/modules/home/pages/admin/admin.module.ts | 4 +--- .../home/pages/mobile/mobile-routing.module.ts | 2 +- .../mobile-app-settings.component.html | 0 .../mobile-app-settings.component.scss | 0 .../mobile-app-settings.component.ts | 2 +- 6 files changed, 3 insertions(+), 18 deletions(-) rename ui-ngx/src/app/modules/home/pages/{admin => mobile/qr-code-widget}/mobile-app-settings.component.html (100%) rename ui-ngx/src/app/modules/home/pages/{admin => mobile/qr-code-widget}/mobile-app-settings.component.scss (100%) rename ui-ngx/src/app/modules/home/pages/{admin => mobile/qr-code-widget}/mobile-app-settings.component.ts (99%) diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index 43a2c588c2..b914ab8b2a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -38,7 +38,6 @@ import { widgetsLibraryRoutes } from '@home/pages/widget/widget-library-routing. import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module'; import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component'; -import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component'; import { oAuth2Routes } from '@home/pages/admin/oauth2/oauth2-routing.module'; import { ImageResourceType, IMAGES_URL_PREFIX, ResourceSubType } from '@shared/models/resource.models'; import { ScadaSymbolComponent } from '@home/pages/scada-symbol/scada-symbol.component'; @@ -312,18 +311,6 @@ const routes: Routes = [ } } }, - { - path: 'mobile-app', - component: MobileAppSettingsComponent, - canDeactivate: [ConfirmOnExitGuard], - data: { - auth: [Authority.SYS_ADMIN], - title: 'admin.mobile-app.mobile-app', - breadcrumb: { - menuId: MenuId.mobile_app_settings - } - } - }, { path: 'security-settings', redirectTo: '/security-settings/general' diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index 1904738299..f00b4d01f1 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -32,7 +32,6 @@ import { QueueComponent } from '@home/pages/admin/queue/queue.component'; import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component'; import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component'; import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component'; -import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component'; import { WidgetComponentsModule } from '@home/components/widget/widget-components.module'; import { OAuth2Module } from '@home/pages/admin/oauth2/oauth2.module'; @@ -50,8 +49,7 @@ import { OAuth2Module } from '@home/pages/admin/oauth2/oauth2.module'; QueueComponent, RepositoryAdminSettingsComponent, AutoCommitAdminSettingsComponent, - TwoFactorAuthSettingsComponent, - MobileAppSettingsComponent + TwoFactorAuthSettingsComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts index 6665c716b9..8aa2e92381 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts @@ -20,7 +20,7 @@ import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { Authority } from '@shared/models/authority.enum'; import { MenuId } from '@core/services/menu.models'; import { MobileAppTableConfigResolver } from '@home/pages/mobile/applications/mobile-app-table-config.resolver'; -import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component'; +import { MobileAppSettingsComponent } from '@home/pages/mobile/qr-code-widget/mobile-app-settings.component'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { applicationsRoutes } from '@home/pages/mobile/applications/applications-routing.module'; import { bundlesRoutes } from '@home/pages/mobile/bundes/bundles-routing.module'; diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-app-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html rename to ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-app-settings.component.html diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-app-settings.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss rename to ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-app-settings.component.scss diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-app-settings.component.ts similarity index 99% rename from ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts rename to ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-app-settings.component.ts index d50a888d4a..ec2e4317a0 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-app-settings.component.ts @@ -32,7 +32,7 @@ import { ActionUpdateMobileQrCodeEnabled } from '@core/auth/auth.actions'; @Component({ selector: 'tb-mobile-app-settings', templateUrl: './mobile-app-settings.component.html', - styleUrls: ['mobile-app-settings.component.scss', './settings-card.scss'] + styleUrls: ['mobile-app-settings.component.scss', '../../admin/settings-card.scss'] }) export class MobileAppSettingsComponent extends PageComponent implements HasConfirmForm, OnDestroy { From cf7161c2efd847caf5e8aad7ad330888ffca4580 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 22 Oct 2024 15:50:16 +0300 Subject: [PATCH 19/48] UI: Rename mobile-app-settings.component --- .../modules/home/pages/mobile/mobile-routing.module.ts | 4 ++-- ...html => mobile-qr-code-widget-settings.component.html} | 0 ...scss => mobile-qr-code-widget-settings.component.scss} | 0 ...ent.ts => mobile-qr-code-widget-settings.component.ts} | 8 ++++---- 4 files changed, 6 insertions(+), 6 deletions(-) rename ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/{mobile-app-settings.component.html => mobile-qr-code-widget-settings.component.html} (100%) rename ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/{mobile-app-settings.component.scss => mobile-qr-code-widget-settings.component.scss} (100%) rename ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/{mobile-app-settings.component.ts => mobile-qr-code-widget-settings.component.ts} (96%) diff --git a/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts index 8aa2e92381..bfc9d4c95a 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts @@ -20,7 +20,7 @@ import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { Authority } from '@shared/models/authority.enum'; import { MenuId } from '@core/services/menu.models'; import { MobileAppTableConfigResolver } from '@home/pages/mobile/applications/mobile-app-table-config.resolver'; -import { MobileAppSettingsComponent } from '@home/pages/mobile/qr-code-widget/mobile-app-settings.component'; +import { MobileQrCodeWidgetSettingsComponent } from '@home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { applicationsRoutes } from '@home/pages/mobile/applications/applications-routing.module'; import { bundlesRoutes } from '@home/pages/mobile/bundes/bundles-routing.module'; @@ -48,7 +48,7 @@ const routes: Routes = [ ...bundlesRoutes, { path: 'mobile-app', - component: MobileAppSettingsComponent, + component: MobileQrCodeWidgetSettingsComponent, canDeactivate: [ConfirmOnExitGuard], data: { auth: [Authority.SYS_ADMIN], diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-app-settings.component.html b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-app-settings.component.html rename to ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-app-settings.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-app-settings.component.scss rename to ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.scss diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts similarity index 96% rename from ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-app-settings.component.ts rename to ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts index ec2e4317a0..a6f9fa3f4f 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-app-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts @@ -30,11 +30,11 @@ import { import { ActionUpdateMobileQrCodeEnabled } from '@core/auth/auth.actions'; @Component({ - selector: 'tb-mobile-app-settings', - templateUrl: './mobile-app-settings.component.html', - styleUrls: ['mobile-app-settings.component.scss', '../../admin/settings-card.scss'] + selector: 'tb-mobile-qr-code-widget', + templateUrl: './mobile-qr-code-widget-settings.component.html', + styleUrls: ['mobile-qr-code-widget-settings.component.scss', '../../admin/settings-card.scss'] }) -export class MobileAppSettingsComponent extends PageComponent implements HasConfirmForm, OnDestroy { +export class MobileQrCodeWidgetSettingsComponent extends PageComponent implements HasConfirmForm, OnDestroy { mobileAppSettingsForm: FormGroup; From 101e46d547e03cad99e03538058c5c3efbb406b8 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 22 Oct 2024 17:17:06 +0300 Subject: [PATCH 20/48] updated QrCodeSettings to have custom app store links --- .../controller/QrCodeSettingsController.java | 4 ++-- .../mobile/qrCodeSettings/QrCodeSettings.java | 4 ++-- .../dao/mobile/QrCodeSettingServiceImpl.java | 15 +++++++++++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java index 547cdc738f..988b5a9f10 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java @@ -179,12 +179,12 @@ public class QrCodeSettingsController extends BaseController { QrCodeSettings qrCodeSettings = qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID); boolean useDefaultApp = qrCodeSettings.isUseDefaultApp(); if (userAgent.contains("Android")) { - String googlePlayLink = useDefaultApp ? qrCodeSettings.getDefaultGooglePlayLink() : getStoreLink(qrCodeSettings.getMobileAppBundleId(), ANDROID); + String googlePlayLink = useDefaultApp ? qrCodeSettings.getGooglePlayLink() : getStoreLink(qrCodeSettings.getMobileAppBundleId(), ANDROID); return ResponseEntity.status(HttpStatus.FOUND) .header("Location", googlePlayLink) .build(); } else if (userAgent.contains("iPhone") || userAgent.contains("iPad")) { - String appStoreLink = useDefaultApp ? qrCodeSettings.getDefaultAppStoreLink() : getStoreLink(qrCodeSettings.getMobileAppBundleId(), IOS); + String appStoreLink = useDefaultApp ? qrCodeSettings.getAppStoreLink() : getStoreLink(qrCodeSettings.getMobileAppBundleId(), IOS); return ResponseEntity.status(HttpStatus.FOUND) .header("Location", appStoreLink) .build(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QrCodeSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QrCodeSettings.java index 550da0e539..0af5e53e5a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QrCodeSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QrCodeSettings.java @@ -44,10 +44,10 @@ public class QrCodeSettings extends BaseData implements HasTen private QRCodeConfig qrCodeConfig; @JsonProperty(access = JsonProperty.Access.READ_ONLY) - private String defaultGooglePlayLink; + private String googlePlayLink; @JsonProperty(access = JsonProperty.Access.READ_ONLY) - private String defaultAppStoreLink; + private String appStoreLink; public QrCodeSettings() { } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java index d217c1c463..b97fdeafd0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java @@ -31,6 +31,8 @@ import org.thingsboard.server.dao.service.DataValidator; import java.util.Map; +import static org.thingsboard.server.common.data.oauth2.PlatformType.ANDROID; +import static org.thingsboard.server.common.data.oauth2.PlatformType.IOS; import static org.thingsboard.server.dao.service.Validator.validateId; @Service @@ -112,8 +114,17 @@ public class QrCodeSettingServiceImpl extends AbstractCachedEntityService Date: Tue, 22 Oct 2024 17:27:57 +0300 Subject: [PATCH 21/48] UI: Add mobile qr code widget settings --- ui-ngx/src/app/core/services/menu.models.ts | 16 +- .../mobile-app-qrcode-widget.component.html | 6 +- .../lib/mobile-app-qrcode-widget.component.ts | 4 +- .../modules/home/pages/admin/admin.module.ts | 2 - .../pages/mobile/mobile-routing.module.ts | 28 +-- .../home/pages/mobile/mobile.module.ts | 4 + ...-qr-code-widget-settings-routing.module.ts | 37 +++ ...ile-qr-code-widget-settings.component.html | 220 +++++++++--------- ...obile-qr-code-widget-settings.component.ts | 150 ++++++------ .../mobile-qr-code-widget-settings.module.ts | 26 +++ .../entity/entity-autocomplete.component.html | 3 +- .../entity/entity-autocomplete.component.ts | 19 +- .../app/shared/models/mobile-app.models.ts | 24 +- .../assets/locale/locale.constant-en_US.json | 8 +- 14 files changed, 302 insertions(+), 245 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings-routing.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.module.ts diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 0a80f4e1de..d3d8e1f399 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -68,7 +68,7 @@ export enum MenuId { mobile_center = 'mobile_center', mobile_apps = 'mobile_apps', mobile_bundles = 'mobile_bundles', - mobile_app_settings = 'mobile_app_settings', + mobile_qr_code_widget = 'mobile_qr_code_widget', settings = 'settings', general = 'general', mail_server = 'mail_server', @@ -304,14 +304,14 @@ export const menuSectionMap = new Map([ } ], [ - MenuId.mobile_app_settings, + MenuId.mobile_qr_code_widget, { - id: MenuId.mobile_app_settings, - name: 'admin.mobile-app.mobile-app', - fullName: 'admin.mobile-app.mobile-app', + id: MenuId.mobile_qr_code_widget, + name: 'mobile.qr-code-widget', + fullName: 'mobile.qr-code-widget', type: 'link', - path: '/mobile-center/mobile-app', - icon: 'smartphone' + path: '/mobile-center/qr-code-widget', + icon: 'qr_code' } ], [ @@ -714,7 +714,7 @@ const defaultUserMenuMap = new Map([ pages: [ {id: MenuId.mobile_apps}, {id: MenuId.mobile_bundles}, - {id: MenuId.mobile_app_settings} + {id: MenuId.mobile_qr_code_widget} ] }, { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html index 1426d02a0d..625fe00a04 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html @@ -27,13 +27,13 @@ [class.row-reverse]="mobileAppSettings?.qrCodeConfig.badgePosition === badgePosition.LEFT">
- + {{ 'widgets.mobile-app-qr-code.download-on-the-app-store' | translate }} - + {{ 'widgets.mobile-app-qr-code.get-it-on-google-play' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts index 875236abb5..e872ce06ce 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts @@ -157,8 +157,8 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI private updateQRCode(link: string) { import('qrcode').then((QRCode) => { - QRCode.toString(link, (err, string) => { - this.qrCodeSVG = string; + QRCode.toString(link, (err, svgElement) => { + this.qrCodeSVG = svgElement; this.cd.markForCheck(); }) }); diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index f00b4d01f1..5613e953c3 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -32,7 +32,6 @@ import { QueueComponent } from '@home/pages/admin/queue/queue.component'; import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component'; import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component'; import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component'; -import { WidgetComponentsModule } from '@home/components/widget/widget-components.module'; import { OAuth2Module } from '@home/pages/admin/oauth2/oauth2.module'; @NgModule({ @@ -56,7 +55,6 @@ import { OAuth2Module } from '@home/pages/admin/oauth2/oauth2.module'; SharedModule, HomeComponentsModule, AdminRoutingModule, - WidgetComponentsModule, OAuth2Module ] }) diff --git a/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts index bfc9d4c95a..a0bc9bec0a 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts @@ -19,11 +19,9 @@ import { RouterModule, Routes } from '@angular/router'; import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { Authority } from '@shared/models/authority.enum'; import { MenuId } from '@core/services/menu.models'; -import { MobileAppTableConfigResolver } from '@home/pages/mobile/applications/mobile-app-table-config.resolver'; -import { MobileQrCodeWidgetSettingsComponent } from '@home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component'; -import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { applicationsRoutes } from '@home/pages/mobile/applications/applications-routing.module'; import { bundlesRoutes } from '@home/pages/mobile/bundes/bundles-routing.module'; +import { qrCodeWidgetRoutes } from '@home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings-routing.module'; const routes: Routes = [ { @@ -46,34 +44,12 @@ const routes: Routes = [ }, ...applicationsRoutes, ...bundlesRoutes, - { - path: 'mobile-app', - component: MobileQrCodeWidgetSettingsComponent, - canDeactivate: [ConfirmOnExitGuard], - data: { - auth: [Authority.SYS_ADMIN], - title: 'admin.mobile-app.mobile-app', - breadcrumb: { - menuId: MenuId.mobile_app_settings - } - } - } + ...qrCodeWidgetRoutes ] } ]; -routes.push( - { - path: 'security-settings/oauth2/mobile-applications', - pathMatch: 'full', - redirectTo: '/mobile-center/applications' - } -); - @NgModule({ - providers: [ - MobileAppTableConfigResolver - ], imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) diff --git a/ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts index 9c47d507c3..49f59aa147 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts @@ -21,6 +21,9 @@ import { HomeComponentsModule } from '@home/components/home-components.module'; import { MobileRoutingModule } from '@home/pages/mobile/mobile-routing.module'; import { MobileApplicationModule } from '@home/pages/mobile/applications/applications.module'; import { MobileBundlesModule } from '@home/pages/mobile/bundes/bundles.module'; +import { + MobileQrCodeWidgetSettingsModule +} from '@home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.module'; @NgModule({ imports: [ @@ -29,6 +32,7 @@ import { MobileBundlesModule } from '@home/pages/mobile/bundes/bundles.module'; HomeComponentsModule, MobileApplicationModule, MobileBundlesModule, + MobileQrCodeWidgetSettingsModule, MobileRoutingModule, ] }) diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings-routing.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings-routing.module.ts new file mode 100644 index 0000000000..98ce17e1d4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings-routing.module.ts @@ -0,0 +1,37 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { Authority } from '@shared/models/authority.enum'; +import { MenuId } from '@core/services/menu.models'; +import { + MobileQrCodeWidgetSettingsComponent +} from '@home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; + +export const qrCodeWidgetRoutes: Routes = [ + { + path: 'qr-code-widget', + component: MobileQrCodeWidgetSettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'mobile.qr-code-widget', + breadcrumb: { + menuId: MenuId.mobile_qr_code_widget + } + } + } +]; + +const routes: Routes = [ + { + path: 'settings/mobile-app', + pathMatch: 'full', + redirectTo: '/mobile-center/qr-code-widget' + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class MobileQrCodeWidgetSettingsRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html index 51e7c6f43d..021fae965c 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html @@ -34,110 +34,122 @@ {{ 'admin.mobile-app.custom' | translate }}
-
-
- - {{ 'admin.mobile-app.android' | translate }} - -
-
-
-
{{ 'admin.mobile-app.app-package-name' | translate }}
- - - - warning - - -
-
-
-
-
{{ 'admin.mobile-app.sha256-certificate-fingerprints' | translate }}
- - - - warning - - -
-
-
-
-
{{ 'admin.mobile-app.google-play-link' | translate }}
- - - - warning - - -
-
-
-
-
- - {{ 'admin.mobile-app.ios' | translate }} - -
-
-
-
{{ 'admin.mobile-app.app-id' | translate }}
- - - - warning - - -
-
-
-
-
{{ 'admin.mobile-app.app-store-link' | translate }}
- - - - warning - - -
-
+
+
{{ 'mobile.bundle' | translate }}
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
@@ -154,7 +166,7 @@ {{ 'admin.mobile-app.badges' | translate }} - + {{ badgePosition.value | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts index a6f9fa3f4f..4f5ffdbe14 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts @@ -28,6 +28,7 @@ import { QrCodeSettings } from '@shared/models/mobile-app.models'; import { ActionUpdateMobileQrCodeEnabled } from '@core/auth/auth.actions'; +import { EntityType } from '@shared/models/entity-type.models'; @Component({ selector: 'tb-mobile-qr-code-widget', @@ -37,12 +38,12 @@ import { ActionUpdateMobileQrCodeEnabled } from '@core/auth/auth.actions'; export class MobileQrCodeWidgetSettingsComponent extends PageComponent implements HasConfirmForm, OnDestroy { mobileAppSettingsForm: FormGroup; - mobileAppSettings: QrCodeSettings; - private readonly destroy$ = new Subject(); + readonly badgePositionTranslationsMap = badgePositionTranslationsMap; + readonly entityType = EntityType; - badgePositionTranslationsMap = badgePositionTranslationsMap; + private readonly destroy$ = new Subject(); constructor(protected store: Store, private mobileAppService: MobileApplicationService, @@ -55,33 +56,36 @@ export class MobileQrCodeWidgetSettingsComponent extends PageComponent implement takeUntil(this.destroy$) ).subscribe(value => { if (value) { - this.mobileAppSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false}); - this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false}); - this.mobileAppSettingsForm.get('androidConfig.storeLink').disable({emitEvent: false}); - this.mobileAppSettingsForm.get('iosConfig.appId').disable({emitEvent: false}); - this.mobileAppSettingsForm.get('iosConfig.storeLink').disable({emitEvent: false}); + this.mobileAppSettingsForm.get('mobileAppBundleId').disable({emitEvent: false}); + // this.mobileAppSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false}); + // this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false}); + // this.mobileAppSettingsForm.get('androidConfig.storeLink').disable({emitEvent: false}); + // this.mobileAppSettingsForm.get('iosConfig.appId').disable({emitEvent: false}); + // this.mobileAppSettingsForm.get('iosConfig.storeLink').disable({emitEvent: false}); } else { - if (this.mobileAppSettingsForm.get('androidConfig.enabled').value) { - this.mobileAppSettingsForm.get('androidConfig.appPackage').enable({emitEvent: false}); - this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').enable({emitEvent: false}); - this.mobileAppSettingsForm.get('androidConfig.storeLink').enable({emitEvent: false}); - } - if (this.mobileAppSettingsForm.get('iosConfig.enabled').value) { - this.mobileAppSettingsForm.get('iosConfig.appId').enable({emitEvent: false}); - this.mobileAppSettingsForm.get('iosConfig.storeLink').enable({emitEvent: false}); - } + this.mobileAppSettingsForm.get('mobileAppBundleId').enable({emitEvent: false}); + + // if (this.mobileAppSettingsForm.get('androidConfig.enabled').value) { + // this.mobileAppSettingsForm.get('androidConfig.appPackage').enable({emitEvent: false}); + // this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').enable({emitEvent: false}); + // this.mobileAppSettingsForm.get('androidConfig.storeLink').enable({emitEvent: false}); + // } + // if (this.mobileAppSettingsForm.get('iosConfig.enabled').value) { + // this.mobileAppSettingsForm.get('iosConfig.appId').enable({emitEvent: false}); + // this.mobileAppSettingsForm.get('iosConfig.storeLink').enable({emitEvent: false}); + // } } }); - this.mobileAppSettingsForm.get('androidConfig.enabled').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe(value => { - this.androidEnableChanged(value); - }); - this.mobileAppSettingsForm.get('iosConfig.enabled').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe(value => { - this.iosEnableChanged(value); - }); + // this.mobileAppSettingsForm.get('androidConfig.enabled').valueChanges.pipe( + // takeUntil(this.destroy$) + // ).subscribe(value => { + // this.androidEnableChanged(value); + // }); + // this.mobileAppSettingsForm.get('iosConfig.enabled').valueChanges.pipe( + // takeUntil(this.destroy$) + // ).subscribe(value => { + // this.iosEnableChanged(value); + // }); this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(value => { @@ -98,13 +102,13 @@ export class MobileQrCodeWidgetSettingsComponent extends PageComponent implement takeUntil(this.destroy$) ).subscribe(value => { if (value) { - if (this.mobileAppSettingsForm.get('androidConfig.enabled').value || this.mobileAppSettingsForm.get('iosConfig.enabled').value) { - this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); - this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable({emitEvent: false}); - } else { - this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); - this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); - } + // if (this.mobileAppSettingsForm.get('androidConfig.enabled').value || this.mobileAppSettingsForm.get('iosConfig.enabled').value) { + // this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); + // this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable({emitEvent: false}); + // } else { + // this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); + // this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); + // } } else { this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); } @@ -129,17 +133,18 @@ export class MobileQrCodeWidgetSettingsComponent extends PageComponent implement private buildMobileAppSettingsForm() { this.mobileAppSettingsForm = this.fb.group({ useDefaultApp: [true], - androidConfig: this.fb.group({ - enabled: [true], - appPackage: [{value: '', disabled: true}, [Validators.required]], - sha256CertFingerprints: [{value: '', disabled: true}, [Validators.required]], - storeLink: ['', [Validators.required]] - }), - iosConfig: this.fb.group({ - enabled: [true], - appId: [{value: '', disabled: true}, [Validators.required]], - storeLink: ['', [Validators.required]] - }), + mobileAppBundleId: [{value: null, disabled: true}, Validators.required], + // androidConfig: this.fb.group({ + // enabled: [true], + // appPackage: [{value: '', disabled: true}, [Validators.required]], + // sha256CertFingerprints: [{value: '', disabled: true}, [Validators.required]], + // storeLink: ['', [Validators.required]] + // }), + // iosConfig: this.fb.group({ + // enabled: [true], + // appId: [{value: '', disabled: true}, [Validators.required]], + // storeLink: ['', [Validators.required]] + // }), qrCodeConfig: this.fb.group({ showOnHomePage: [true], badgeEnabled: [true], @@ -155,33 +160,33 @@ export class MobileQrCodeWidgetSettingsComponent extends PageComponent implement this.mobileAppSettingsForm.reset(this.mobileAppSettings); } - private androidEnableChanged(value: boolean): void { - if (value) { - if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { - this.mobileAppSettingsForm.get('androidConfig.appPackage').enable({emitEvent: false}); - this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').enable({emitEvent: false}); - this.mobileAppSettingsForm.get('androidConfig.storeLink').enable({emitEvent: false}); - } - } else { - this.mobileAppSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false}); - this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false}); - this.mobileAppSettingsForm.get('androidConfig.storeLink').disable({emitEvent: false}); - } - this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').updateValueAndValidity({onlySelf: true}); - } - - private iosEnableChanged(value: boolean): void { - if (value) { - if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { - this.mobileAppSettingsForm.get('iosConfig.appId').enable({emitEvent: false}); - this.mobileAppSettingsForm.get('iosConfig.storeLink').enable({emitEvent: false}); - } - } else { - this.mobileAppSettingsForm.get('iosConfig.appId').disable({emitEvent: false}); - this.mobileAppSettingsForm.get('iosConfig.storeLink').disable({emitEvent: false}); - } - this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').updateValueAndValidity({onlySelf: true}); - } + // private androidEnableChanged(value: boolean): void { + // if (value) { + // if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { + // this.mobileAppSettingsForm.get('androidConfig.appPackage').enable({emitEvent: false}); + // this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').enable({emitEvent: false}); + // this.mobileAppSettingsForm.get('androidConfig.storeLink').enable({emitEvent: false}); + // } + // } else { + // this.mobileAppSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false}); + // this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false}); + // this.mobileAppSettingsForm.get('androidConfig.storeLink').disable({emitEvent: false}); + // } + // this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').updateValueAndValidity({onlySelf: true}); + // } + // + // private iosEnableChanged(value: boolean): void { + // if (value) { + // if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { + // this.mobileAppSettingsForm.get('iosConfig.appId').enable({emitEvent: false}); + // this.mobileAppSettingsForm.get('iosConfig.storeLink').enable({emitEvent: false}); + // } + // } else { + // this.mobileAppSettingsForm.get('iosConfig.appId').disable({emitEvent: false}); + // this.mobileAppSettingsForm.get('iosConfig.storeLink').disable({emitEvent: false}); + // } + // this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').updateValueAndValidity({onlySelf: true}); + // } save(): void { const showOnHomePagePreviousValue = this.mobileAppSettings.qrCodeConfig.showOnHomePage; @@ -199,5 +204,4 @@ export class MobileQrCodeWidgetSettingsComponent extends PageComponent implement confirmForm(): FormGroup { return this.mobileAppSettingsForm; } - } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.module.ts new file mode 100644 index 0000000000..05e1d853c3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeComponentsModule } from '@home/components/home-components.module'; +import { + MobileQrCodeWidgetSettingsComponent +} from '@home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component'; +import { WidgetComponentsModule } from '@home/components/widget/widget-components.module'; +import { + MobileQrCodeWidgetSettingsRoutingModule +} from '@home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings-routing.module'; + + +@NgModule({ + declarations: [ + MobileQrCodeWidgetSettingsComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + WidgetComponentsModule, + MobileQrCodeWidgetSettingsRoutingModule + ] +}) +export class MobileQrCodeWidgetSettingsModule { } diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html index c860835026..cd0e7f2d1a 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html @@ -15,7 +15,8 @@ limitations under the License. --> - + {{ label | translate }} ; + @Output() entityChanged = new EventEmitter>(); @@ -274,11 +281,17 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit this.notFoundEntities = 'queue-statistics.no-queue-statistics-text'; break; case EntityType.MOBILE_APP: - this.entityText = 'mobile.applications'; + this.entityText = 'mobile.application'; this.noEntitiesMatchingText = 'mobile.no-application-matching'; this.entityRequiredText = 'mobile.application-required'; this.notFoundEntities = 'mobile.no-application-text'; break; + case EntityType.MOBILE_APP_BUNDLE: + this.entityText = 'mobile.bundle'; + this.noEntitiesMatchingText = 'mobile.no-bundle-matching'; + this.entityRequiredText = 'mobile.bundle-required'; + this.notFoundEntities = 'mobile.no-bundle-text'; + break; case AliasEntityType.CURRENT_CUSTOMER: this.entityText = 'customer.default-customer'; this.noEntitiesMatchingText = 'customer.no-customers-matching'; diff --git a/ui-ngx/src/app/shared/models/mobile-app.models.ts b/ui-ngx/src/app/shared/models/mobile-app.models.ts index ce07a1ef71..bf1664768a 100644 --- a/ui-ngx/src/app/shared/models/mobile-app.models.ts +++ b/ui-ngx/src/app/shared/models/mobile-app.models.ts @@ -24,8 +24,8 @@ import { deepClone, isNotEmptyStr } from '@core/utils'; export interface QrCodeSettings extends HasTenantId { useDefaultApp: boolean; mobileAppBundleId: MobileAppBundleId - androidConfig: AndroidConfig; //TODO: need remove - iosConfig: IosConfig; //TODO: need remove + androidConfig: any; //TODO: need remove + iosConfig: any; //TODO: need remove qrCodeConfig: QRCodeConfig; defaultGooglePlayLink: string; defaultAppStoreLink: string; @@ -34,19 +34,6 @@ export interface QrCodeSettings extends HasTenantId { } } -export interface AndroidConfig { - enabled: boolean; - appPackage: string; - sha256CertFingerprints: string; - storeLink: string; -} - -export interface IosConfig { - enabled: boolean; - appId: string; - storeLink: string; -} - export interface QRCodeConfig { showOnHomePage: boolean; badgeEnabled: boolean; @@ -55,11 +42,6 @@ export interface QRCodeConfig { qrCodeLabel: string; } -export interface MobileOSBadgeURL { - iOS: string; - android: string; -} - export enum BadgePosition { RIGHT = 'RIGHT', LEFT = 'LEFT' @@ -70,8 +52,6 @@ export const badgePositionTranslationsMap = new Map([ [BadgePosition.LEFT, 'admin.mobile-app.left'] ]); -export type QrCodeConfig = AndroidConfig & IosConfig; - export enum MobileAppStatus { DRAFT = 'DRAFT', PUBLISHED = 'PUBLISHED', diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 1e6e58487e..90a55616ae 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4071,6 +4071,7 @@ "application-package": "Application Package", "application-secret": "Application Secret", "application-secret-required": "Application Secret is required", + "application": "Application", "applications": "Applications", "copy-app-id": "Copy App ID", "copy-app-store-link": "Copy App Store link", @@ -4111,6 +4112,7 @@ "version-information": "Version information", "min-version-release-notes": "Min version release notes", "latest-version-release-notes": "Latest version release notes", + "bundle": "Bundle", "bundles": "Bundles", "add-bundle": "Add bundle", "title": "Title", @@ -4126,8 +4128,11 @@ "description": "Description", "basic-settings": "Basic settings", "no-application-matching": "No application matching '{{entity}}' were found.", + "no-bundle-matching": "No bundle matching '{{entity}}' were found.", "application-required": "Application is required.", + "bundle-required": "Bundle is required.", "no-application-text": "No applications found", + "no-bundle-text": "No bundle found", "layout": "Layout", "pages": "Pages", "hide-all-pages": "Hide all pages", @@ -4152,7 +4157,8 @@ "custom-page": "Custom page", "edit-page": "Edit page", "edit-custom-page": "Edit custom page", - "delete-page": "Delete page" + "delete-page": "Delete page", + "qr-code-widget": "QR code widget" }, "notification": { "action-button": "Action button", From 6a4adf759f1997c626a034f9bec4a26d15b07bc4 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 22 Oct 2024 17:37:35 +0300 Subject: [PATCH 22/48] fixed oauth2 client permissions --- .../org/thingsboard/server/controller/OAuth2Controller.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index a235034756..c6bd551b7d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -128,7 +128,6 @@ public class OAuth2Controller extends BaseController { @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CLIENT, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return oAuth2ClientService.findOAuth2ClientInfosByTenantId(getTenantId(), pageLink); } @@ -168,8 +167,7 @@ public class OAuth2Controller extends BaseController { "as 'SECURITY_OAUTH2_LOGIN_PROCESSING_URL' env variable. By default it is '/login/oauth2/code/'" + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/oauth2/loginProcessingUrl") - public String getLoginProcessingUrl() throws ThingsboardException { - accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CLIENT, Operation.READ); + public String getLoginProcessingUrl() { return "\"" + oAuth2Configuration.getLoginProcessingUrl() + "\""; } From 71735ab41497a81973722494cbafb28a8776f9fa Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 22 Oct 2024 17:30:26 +0300 Subject: [PATCH 23/48] UI: Add mobile center in tenant level --- ui-ngx/src/app/core/services/menu.models.ts | 16 +++++++++++++++- .../pages/admin/oauth2/oauth2-routing.module.ts | 13 ++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index d3d8e1f399..38814b10ad 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -804,6 +804,14 @@ const defaultUserMenuMap = new Map([ {id: MenuId.notification_rules} ] }, + { + id: MenuId.mobile_center, + pages: [ + {id: MenuId.mobile_apps}, + {id: MenuId.mobile_bundles}, + {id: MenuId.mobile_qr_code_widget} + ] + }, {id: MenuId.api_usage}, { id: MenuId.settings, @@ -817,7 +825,13 @@ const defaultUserMenuMap = new Map([ { id: MenuId.security_settings, pages: [ - {id: MenuId.audit_log} + {id: MenuId.audit_log}, + { + id: MenuId.oauth2, + pages: [ + {id: MenuId.clients} + ] + } ] } ] diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts index 9286304312..a45c1440a7 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts @@ -44,7 +44,7 @@ export const oAuth2Routes: Routes = [ path: 'oauth2', component: RouterTabsComponent, data: { - auth: [Authority.SYS_ADMIN], + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], breadcrumb: { label: 'admin.oauth2.oauth2', icon: 'mdi:shield-account' @@ -55,8 +55,11 @@ export const oAuth2Routes: Routes = [ path: '', children: [], data: { - auth: [Authority.SYS_ADMIN], - redirectTo: '/security-settings/oauth2/domains' + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + redirectTo: { + SYS_ADMIN: '/security-settings/oauth2/domains', + TENANT_ADMIN: '/security-settings/oauth2/clients' + } } }, { @@ -86,7 +89,7 @@ export const oAuth2Routes: Routes = [ path: '', component: EntitiesTableComponent, data: { - auth: [Authority.SYS_ADMIN], + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], title: 'admin.oauth2.clients' }, resolve: { @@ -104,7 +107,7 @@ export const oAuth2Routes: Routes = [ labelFunction: entityDetailsPageBreadcrumbLabelFunction, icon: 'public' } as BreadCrumbConfig, - auth: [Authority.SYS_ADMIN], + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], title: 'admin.oauth2.clients', hideTabs: true, backNavigationCommands: ['../..'] From 4a2bd7fc707aac8a05464e8b3fad90ec25c8874e Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 22 Oct 2024 17:49:59 +0300 Subject: [PATCH 24/48] UI: Add OAuth client to tenant level --- .../security/permission/TenantAdminPermissions.java | 1 + .../home/pages/admin/oauth2/clients/client.component.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index c63121e60b..9928cfeb04 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -52,6 +52,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.NOTIFICATION, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ)); put(Resource.OAUTH2_CLIENT, tenantEntityPermissionChecker); + put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, new PermissionChecker.GenericPermissionChecker(Operation.READ)); put(Resource.MOBILE_APP, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); } diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts index 44a5993485..9cd5205aa2 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts @@ -40,6 +40,8 @@ import { Subscription } from 'rxjs'; import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { PageLink } from '@shared/models/page/page-link'; import { coerceBoolean } from '@app/shared/decorators/coercion'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; @Component({ selector: 'tb-client', @@ -92,13 +94,16 @@ export class ClientComponent extends EntityComponent, + protected entitiesTableConfigValue: EntityTableConfig, protected cd: ChangeDetectorRef, public fb: UntypedFormBuilder) { super(store, fb, entityValue, entitiesTableConfigValue, cd); this.oauth2Service.getOAuth2Template().subscribe(templates => { this.initTemplates(templates); }); + if (getCurrentAuthUser(this.store).authority === Authority.TENANT_ADMIN) { + this.platformTypes = this.platformTypes.filter(item => item !== PlatformType.WEB); + } } ngOnDestroy() { From 4e2be7ab92586e43aa5eb8b8e384e002b75db1c8 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 23 Oct 2024 11:47:10 +0300 Subject: [PATCH 25/48] fixed license check --- .../main/data/upgrade/3.8.0/schema_update.sql | 4 +-- .../thingsboard/server/common/data/Views.java | 35 ++++++------------- .../common/data/mobile/layout/MobilePage.java | 35 ++++++------------- .../sql/mobile/MobileAppBundleRepository.java | 6 ++-- .../dao/sql/mobile/MobileAppRepository.java | 4 +-- 5 files changed, 27 insertions(+), 57 deletions(-) diff --git a/application/src/main/data/upgrade/3.8.0/schema_update.sql b/application/src/main/data/upgrade/3.8.0/schema_update.sql index 6d71efd831..807322052d 100644 --- a/application/src/main/data/upgrade/3.8.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.0/schema_update.sql @@ -102,7 +102,7 @@ $$ qrCodeRecord RECORD; BEGIN -- in case of running the upgrade script a second time - IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'qr_code_settings' and column_name = 'android_config') THEN + IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'qr_code_settings' AND column_name = 'android_config') THEN FOR qrCodeRecord IN SELECT * FROM qr_code_settings LOOP generatedBundleId := NULL; @@ -124,7 +124,7 @@ $$ -- migrate ios config iosPkgName := substring(qrCodeRecord.ios_config::jsonb ->> 'appId', strpos(qrCodeRecord.ios_config::jsonb ->> 'appId', '.') + 1); - SELECT id into iosAppId FROM mobile_app WHERE pkg_name = iosPkgName AND platform_type = 'IOS'; + SELECT id INTO iosAppId FROM mobile_app WHERE pkg_name = iosPkgName AND platform_type = 'IOS'; IF iosAppId IS NULL THEN iosAppId := uuid_generate_v4(); INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, store_info) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Views.java b/common/data/src/main/java/org/thingsboard/server/common/data/Views.java index 498d96fdeb..9694ad56ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Views.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Views.java @@ -1,32 +1,17 @@ /** - * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL + * Copyright © 2016-2024 The Thingsboard Authors * - * Copyright © 2016-2024 ThingsBoard, Inc. All Rights Reserved. + * 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 * - * NOTICE: All information contained herein is, and remains - * the property of ThingsBoard, Inc. and its suppliers, - * if any. The intellectual and technical concepts contained - * herein are proprietary to ThingsBoard, Inc. - * and its suppliers and may be covered by U.S. and Foreign Patents, - * patents in process, and are protected by trade secret or copyright law. + * http://www.apache.org/licenses/LICENSE-2.0 * - * Dissemination of this information or reproduction of this material is strictly forbidden - * unless prior written permission is obtained from COMPANY. - * - * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, - * managers or contractors who have executed Confidentiality and Non-disclosure agreements - * explicitly covering such access. - * - * The copyright notice above does not evidence any actual or intended publication - * or disclosure of this source code, which includes - * information that is confidential and/or proprietary, and is a trade secret, of COMPANY. - * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, - * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT - * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, - * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. - * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION - * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, - * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. + * 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; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java index 2491c4e752..fd909202e1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java @@ -1,32 +1,17 @@ /** - * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL + * Copyright © 2016-2024 The Thingsboard Authors * - * Copyright © 2016-2024 ThingsBoard, Inc. All Rights Reserved. + * 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 * - * NOTICE: All information contained herein is, and remains - * the property of ThingsBoard, Inc. and its suppliers, - * if any. The intellectual and technical concepts contained - * herein are proprietary to ThingsBoard, Inc. - * and its suppliers and may be covered by U.S. and Foreign Patents, - * patents in process, and are protected by trade secret or copyright law. + * http://www.apache.org/licenses/LICENSE-2.0 * - * Dissemination of this information or reproduction of this material is strictly forbidden - * unless prior written permission is obtained from COMPANY. - * - * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, - * managers or contractors who have executed Confidentiality and Non-disclosure agreements - * explicitly covering such access. - * - * The copyright notice above does not evidence any actual or intended publication - * or disclosure of this source code, which includes - * information that is confidential and/or proprietary, and is a trade secret, of COMPANY. - * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, - * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT - * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, - * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. - * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION - * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, - * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.thingsboard.server.common.data.mobile.layout; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java index 9aff6af3f1..c3aeb6c32d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java @@ -33,8 +33,8 @@ public interface MobileAppBundleRepository extends JpaRepository findInfoByTenantId(@Param("tenantId") UUID tenantId, @@ -51,7 +51,7 @@ public interface MobileAppBundleRepository extends JpaRepository Date: Thu, 24 Oct 2024 13:40:40 +0300 Subject: [PATCH 26/48] UI: Add license header --- ...ile-qr-code-widget-settings-routing.module.ts | 16 ++++++++++++++++ .../mobile-qr-code-widget-settings.module.ts | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings-routing.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings-routing.module.ts index 98ce17e1d4..bc037ba94d 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings-routing.module.ts @@ -1,3 +1,19 @@ +/// +/// 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. +/// + import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { Authority } from '@shared/models/authority.enum'; diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.module.ts index 05e1d853c3..597e67d1fc 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.module.ts @@ -1,3 +1,19 @@ +/// +/// 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. +/// + import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; From 714152ce91918cf7be585fb8ff771c42775a3b49 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 24 Oct 2024 16:28:14 +0300 Subject: [PATCH 27/48] UI: Remove flexLayout in mobile center --- .../mobile-app-dialog.component.html | 6 +++--- .../mobile-app-table-header.component.html | 8 +++----- .../mobile-app-table-header.component.scss | 5 ----- .../applications/mobile-app.component.html | 14 +++++++------- .../layout/add-mobile-page-dialog.component.html | 7 +++---- .../layout/custom-mobile-page.component.html | 10 +++++----- .../default-mobile-page-panel.component.html | 4 ++-- .../bundes/layout/mobile-layout.component.html | 16 ++++++---------- .../bundes/layout/mobile-layout.component.scss | 6 ------ .../layout/mobile-page-item-row.component.html | 6 +++--- .../bundes/mobile-bundle-dialog.component.html | 8 ++++---- .../mobile-bundle-table-header.component.html | 8 +++----- .../mobile-bundle-table-header.component.scss | 5 ----- ...mobile-qr-code-widget-settings.component.html | 2 +- 14 files changed, 40 insertions(+), 65 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html index f3b0e82aca..80f48838ab 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html @@ -18,7 +18,7 @@

{{ 'mobile.add-application' | translate }}

- +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.scss index 06f14e4712..17ca4ae7c7 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.scss +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.scss @@ -15,9 +15,4 @@ */ :host{ width: 100000px; - - .tb-entity-title-container { - display: flex; - align-items: center; - } } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html index 256d658ee2..99460c8a6f 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
mobile.mobile-package @@ -82,9 +82,9 @@
mobile.version-information
-
-
- +
+
+ mobile.min-version @@ -98,8 +98,8 @@ mdi:text-box-edit
-
- +
+ mobile.latest-version @@ -117,7 +117,7 @@
mobile.store-information
-
+
{{ (entityForm.get('platformType').value === PlatformType.ANDROID ? 'mobile.google-play-link' : 'mobile.app-store-link') | translate diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.html index 2963976afe..a4c888c8e8 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.html @@ -15,20 +15,19 @@ limitations under the License. --> - +

mobile.add-specific-page

-
-
+
-
+
-
+
- + mobile.page-name @@ -41,7 +41,7 @@ - - + mobile.url - + mobile.path diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html index 8345facaed..3ed1601a03 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html @@ -35,12 +35,12 @@ {{ 'mobile.visible' | translate }}
-
+
- + mobile.page-name diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html index 4bc2f90b44..ebda8d4f0c 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html @@ -16,21 +16,19 @@ -->
-
+
mobile.pages
- - -
-
- +
+ + class="tb-mobile-page-item-info flex" appearance="outline" subscriptSizing="dynamic">
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html index 9d7fb0ca4e..df814b710b 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html @@ -17,7 +17,7 @@ -->

{{ dialogTitle | translate }}

- +
- + diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.html index 0f4f435a15..a30aa81e4f 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.html @@ -15,14 +15,12 @@ limitations under the License. --> -
-
+
+
mobile.bundles
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.scss index 06f14e4712..17ca4ae7c7 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.scss +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.scss @@ -15,9 +15,4 @@ */ :host{ width: 100000px; - - .tb-entity-title-container { - display: flex; - align-items: center; - } } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html index 021fae965c..d1123eae74 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html @@ -37,7 +37,7 @@
{{ 'mobile.bundle' | translate }}
Date: Thu, 24 Oct 2024 18:12:06 +0300 Subject: [PATCH 28/48] UI: Add remove and config mobile app dialog; Fixed input colors --- .../applications/applications.module.ts | 6 ++ ...le-app-configuration-dialog.component.html | 36 ++++++++ ...le-app-configuration-dialog.component.scss | 22 +++++ ...bile-app-configuration-dialog.component.ts | 58 +++++++++++++ .../mobile-app-table-config.resolver.ts | 85 ++++++++++++++++++- .../remove-app-dialog.component.html | 48 +++++++++++ .../remove-app-dialog.component.scss | 24 ++++++ .../remove-app-dialog.component.ts | 68 +++++++++++++++ .../layout/custom-mobile-page.component.scss | 2 + .../default-mobile-page-panel.component.scss | 1 + .../mobile-bundle-dialog.component.scss | 1 + .../assets/locale/locale.constant-en_US.json | 11 ++- 12 files changed, 355 insertions(+), 7 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.ts diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts index eae2e5e278..d8e619f865 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts @@ -23,6 +23,10 @@ import { HomeComponentsModule } from '@home/components/home-components.module'; import { ApplicationsRoutingModule } from '@home/pages/mobile/applications/applications-routing.module'; import { ReleaseNotesPanelComponent } from '@home/pages/mobile/applications/release-notes-panel.component'; import { MobileAppDialogComponent } from '@home/pages/mobile/applications/mobile-app-dialog.component'; +import { RemoveAppDialogComponent } from '@home/pages/mobile/applications/remove-app-dialog.component'; +import { + MobileAppConfigurationDialogComponent +} from '@home/pages/mobile/applications/mobile-app-configuration-dialog.component'; @NgModule({ declarations: [ @@ -30,6 +34,8 @@ import { MobileAppDialogComponent } from '@home/pages/mobile/applications/mobile MobileAppTableHeaderComponent, ReleaseNotesPanelComponent, MobileAppDialogComponent, + RemoveAppDialogComponent, + MobileAppConfigurationDialogComponent, ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.html new file mode 100644 index 0000000000..5689e40f1e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.html @@ -0,0 +1,36 @@ + + +

mobile.configuration-dialog

+ +
+
+ +
+
+ {{ 'action.dont-show-again' | translate}} + + +
+ diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.scss new file mode 100644 index 0000000000..129f5c06de --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.scss @@ -0,0 +1,22 @@ +/** + * 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. + */ +:host { + height: 100%; + max-height: 100vh; + display: grid; + width: 800px; + grid-template-rows: min-content minmax(auto, 1fr) min-content; +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.ts new file mode 100644 index 0000000000..9721396937 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.ts @@ -0,0 +1,58 @@ +/// +/// 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. +/// + +import { Component, Inject } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +export interface MobileAppConfigurationDialogData { + afterAdd: boolean; +} + +@Component({ + selector: 'tb-mobile-app-configuration-dialog', + templateUrl: './mobile-app-configuration-dialog.component.html', + styleUrls: ['./mobile-app-configuration-dialog.component.scss'] +}) +export class MobileAppConfigurationDialogComponent extends DialogComponent { + + notShowAgain = false; + + showDontShowAgain: boolean; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) private data: MobileAppConfigurationDialogData, + protected dialogRef: MatDialogRef, + ) { + super(store, router, dialogRef); + + this.showDontShowAgain = this.data.afterAdd; + + } + + close(): void { + if (this.notShowAgain && this.showDontShowAgain) { + // this.store.dispatch(new ActionPreferencesPutUserSettings({ notDisplayConnectivityAfterAddDevice: true })); + this.dialogRef.close(null); + } else { + this.dialogRef.close(null); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts index ef513728a9..8dd37f7c35 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts @@ -17,6 +17,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot } from '@angular/router'; import { + CellActionDescriptor, CellActionDescriptorType, DateEntityTableColumn, EntityTableColumn, @@ -29,9 +30,28 @@ import { Direction } from '@app/shared/models/page/sort-order'; import { MobileAppService } from '@core/http/mobile-app.service'; import { MobileAppComponent } from '@home/pages/mobile/applications/mobile-app.component'; import { MobileAppTableHeaderComponent } from '@home/pages/mobile/applications/mobile-app-table-header.component'; -import { MobileApp, MobileAppStatus, mobileAppStatusTranslations } from '@shared/models/mobile-app.models'; +import { + MobileApp, + MobileAppBundleInfo, + MobileAppStatus, + mobileAppStatusTranslations +} from '@shared/models/mobile-app.models'; import { platformTypeTranslations } from '@shared/models/oauth2.models'; import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { NotificationTemplate } from '@shared/models/notification.models'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { + MobileBundleDialogComponent, + MobileBundleDialogData +} from '@home/pages/mobile/bundes/mobile-bundle-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { + MobileAppDeleteDialogData, + RemoveAppDialogComponent +} from '@home/pages/mobile/applications/remove-app-dialog.component'; +import { + MobileAppConfigurationDialogComponent, MobileAppConfigurationDialogData +} from '@home/pages/mobile/applications/mobile-app-configuration-dialog.component'; @Injectable() export class MobileAppTableConfigResolver { @@ -41,10 +61,12 @@ export class MobileAppTableConfigResolver { constructor(private translate: TranslateService, private datePipe: DatePipe, private mobileAppService: MobileAppService, - private truncatePipe: TruncatePipe) { + private truncatePipe: TruncatePipe, + private dialog: MatDialog,) { this.config.selectionEnabled = false; this.config.entityType = EntityType.MOBILE_APP; this.config.addEnabled = false; + this.config.entitiesDeleteEnabled = false; this.config.rowPointer = true; this.config.entityTranslations = entityTypeTranslations.get(EntityType.MOBILE_APP); this.config.entityResources = entityTypeResources.get(EntityType.MOBILE_APP); @@ -97,18 +119,73 @@ export class MobileAppTableConfigResolver { (entity) => entity.versionInfo?.latestVersion ?? '', () => ({}), false), ); - this.config.deleteEntityTitle = (app) => this.translate.instant('mobile.delete-applications-title', {applicationName: app.pkgName}); - this.config.deleteEntityContent = () => this.translate.instant('mobile.delete-applications-text'); this.config.entitiesFetchFunction = pageLink => this.mobileAppService.getTenantMobileAppInfos(pageLink); this.config.loadEntity = id => this.mobileAppService.getMobileAppInfoById(id.id); this.config.saveEntity = (mobileApp) => this.mobileAppService.saveMobileApp(mobileApp); this.config.deleteEntity = id => this.mobileAppService.deleteMobileApp(id.id); + + this.config.cellActionDescriptors = this.configureCellActions(); } resolve(_route: ActivatedRouteSnapshot): EntityTableConfig { return this.config; } + private configureCellActions(): Array> { + return [ + { + name: this.translate.instant('mobile.configuration-app'), + icon: 'code', + isEnabled: () => true, + onAction: ($event, entity) => this.configurationApp($event, entity) + }, + { + name: this.translate.instant('action.delete'), + icon: 'delete', + isEnabled: () => true, + onAction: ($event, entity) => this.deleteEntity($event, entity) + } + ]; + } + + private deleteEntity($event: Event, entity: MobileApp) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(RemoveAppDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + id: entity.id.id + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.updateData(); + } + }); + } + + private configurationApp($event: Event, entity: MobileApp, afterAdd = false) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(MobileAppConfigurationDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + afterAdd + } + }).afterClosed() + .subscribe(() => { + if (afterAdd) { + this.config.updateData(); + } + }); + } + private mobileStatus(status: MobileAppStatus): string { const translateKey = mobileAppStatusTranslations.get(status); let backgroundColor = 'rgba(25, 128, 56, 0.06)'; diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html new file mode 100644 index 0000000000..faca470cdf --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html @@ -0,0 +1,48 @@ + + +

mobile.delete-application

+ +
+ + +
+
+
+ + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.scss new file mode 100644 index 0000000000..de6f669e64 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.scss @@ -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. + */ +:host{ + width: 750px; + height: 100%; + max-width: 100%; + max-height: 100vh; + display: grid; + grid-template-rows: min-content 4px minmax(auto, 1fr) min-content; + --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.ts new file mode 100644 index 0000000000..112b78bb5c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.ts @@ -0,0 +1,68 @@ +/// +/// 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. +/// + +import { Component, Inject } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { TranslateService } from '@ngx-translate/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { FormBuilder } from '@angular/forms'; +import { MobileAppService } from '@core/http/mobile-app.service'; + +export interface MobileAppDeleteDialogData { + id: string; +} + +@Component({ + selector: 'tb-remove-app-dialog', + templateUrl: './remove-app-dialog.component.html', + styleUrls: ['./remove-app-dialog.component.scss'] +}) +export class RemoveAppDialogComponent extends DialogComponent { + + readonly deleteApplicationText: SafeHtml; + readonly deleteVerificationText: string; + + deleteVerification = this.fb.control(''); + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) private data: MobileAppDeleteDialogData, + private translate: TranslateService, + private sanitizer: DomSanitizer, + private fb: FormBuilder, + private mobileAppService: MobileAppService,) { + super(store, router, dialogRef); + this.deleteVerificationText = this.translate.instant('mobile.delete-application-phrase'); + this.deleteApplicationText = this.sanitizer.bypassSecurityTrustHtml( + this.translate.instant('mobile.delete-application-text', {phrase: this.deleteVerificationText}) + ) + } + + cancel(): void { + this.dialogRef.close(false); + } + + delete(): void { + this.mobileAppService.deleteMobileApp(this.data.id).subscribe(() => { + this.dialogRef.close(true); + }); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.scss index 5fda87092d..641e7c877d 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.scss +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.scss @@ -14,6 +14,8 @@ * limitations under the License. */ :host{ + --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); + .mobile-page-form { display: flex; flex-direction: column; diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.scss index a8435cb97e..875e7d8703 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.scss +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.scss @@ -20,6 +20,7 @@ display: flex; flex-direction: column; gap: 16px; + --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); @media #{$mat-lt-md} { width: 90vw; } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.scss index 3649f88d5c..a93cd1026e 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.scss @@ -22,6 +22,7 @@ max-height: 100vh; display: grid; grid-template-rows: min-content 4px minmax(auto, 1fr) min-content; + --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); .mat-mdc-slide-toggle { margin-bottom: 16px; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 2dc78fa6ab..0ec7890aed 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3405,8 +3405,10 @@ "copy-application-secret": "Copy application secret", "copy-google-play-link": "Copy Google Play link", "copy-sha256-certificate-fingerprints": "Copy SHA256 certificate fingerprints", - "delete-applications-text": "Be careful, after the confirmation the mobile application and all related data will become unrecoverable.", - "delete-applications-title": "Are you sure you want to delete the mobile application '{{applicationName}}'?", + "delete-application": "Delete application", + "delete-application-button-text": "I understand consequences, delete application", + "delete-application-text": "This action can not be undone. This will permanently delete your application.
If you don’t want to delete it permanently you can suspend application temporary.
To delete the application anyway please type \"{{phrase}}\" to confirm.", + "delete-application-phrase": "delete application", "delete-applications-bundle-text": "Be careful, after the confirmation the mobile bundle and all related data will become unrecoverable.", "delete-applications-bundle-title": "Are you sure you want to delete the mobile bundle '{{bundleName}}'?", "generate-application-secret": "Generate application secret", @@ -3484,7 +3486,10 @@ "edit-page": "Edit page", "edit-custom-page": "Edit custom page", "delete-page": "Delete page", - "qr-code-widget": "QR code widget" + "qr-code-widget": "QR code widget", + "type-here": "Type hero", + "configuration-dialog": "Configuration dialog", + "configuration-app": "Configuration app" }, "notification": { "action-button": "Action button", From c6396e40742ae6efcd8923a64e22ca29e54b7764 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 25 Oct 2024 11:42:45 +0300 Subject: [PATCH 29/48] UI: Add mobile configure commands and add useSystemSettings in qr code widget --- .../mobile/qrCodeSettings/QrCodeSettings.java | 2 + ...le-app-configuration-dialog.component.html | 49 +++++++++++- ...le-app-configuration-dialog.component.scss | 76 ++++++++++++++++++- ...bile-app-configuration-dialog.component.ts | 29 ++++++- .../mobile-app-table-config.resolver.ts | 23 +++++- ...ile-qr-code-widget-settings.component.html | 9 ++- ...obile-qr-code-widget-settings.component.ts | 53 ++++++++----- .../app/shared/models/mobile-app.models.ts | 1 + .../app/shared/models/user-settings.models.ts | 1 + .../assets/locale/locale.constant-en_US.json | 15 +++- ui-ngx/src/styles.scss | 3 + 11 files changed, 232 insertions(+), 29 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QrCodeSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QrCodeSettings.java index 0af5e53e5a..ce30e9c8ed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QrCodeSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QrCodeSettings.java @@ -35,6 +35,8 @@ public class QrCodeSettings extends BaseData implements HasTen @Schema(description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY) private TenantId tenantId; + @Schema(description = "Use settings from system level", example = "true") + private boolean useSystemSettings; @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Type of application: true means use default Thingsboard app", example = "true") private boolean useDefaultApp; @Schema(description = "Mobile app bundle.") diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.html index 5689e40f1e..17108aafe2 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-configuration-dialog.component.html @@ -23,10 +23,53 @@ close -
- +
+
+
mobile.configuration-step.prepare-environment-title
+
+
mobile.configuration-step.prepare-environment-text
+ + description{{ 'common.documentation' | translate }} + +
+
+
+
mobile.configuration-step.get-source-code-title
+
mobile.configuration-step.get-source-code-text
+ +
+
+
mobile.configuration-step.configure-api-title
+
mobile.configuration-step.configure-api-text
+ +
mobile.configuration-step.configure-api-hint
+ +
+
+
mobile.configuration-step.run-app-title
+
mobile.configuration-step.run-app-text
+ +
+
-
+
{{ 'action.dont-show-again' | translate}} -
+

{{ dialogTitle | translate }}

-
+
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.scss similarity index 91% rename from ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.scss rename to ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.scss index f70487a708..00a2ebca2d 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.scss +++ b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.scss @@ -15,7 +15,7 @@ */ @import '../../../../../../scss/constants'; -.tb-release-notes-panel { +.tb-editor-panel { width: 600px; display: flex; flex-direction: column; @@ -23,14 +23,17 @@ @media #{$mat-lt-md} { width: 90vw; } - .tb-release-notes-title { + .tb-editor-title { font-size: 16px; font-weight: 500; line-height: 24px; letter-spacing: 0.25px; color: rgba(0, 0, 0, 0.87); } - .tb-release-notes-panel-buttons { + .tb-editor { + height: 400px; + } + .tb-editor-buttons { height: 40px; display: flex; flex-direction: row; diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts similarity index 69% rename from ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.ts rename to ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts index 935e949f3d..966ebd6411 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts @@ -20,30 +20,28 @@ import { TbPopoverComponent } from '@shared/components/popover.component'; @Component({ selector: 'tb-release-notes-panel', - templateUrl: './release-notes-panel.component.html', - styleUrls: ['./release-notes-panel.component.scss'], + templateUrl: './editor-panel.component.html', + styleUrls: ['./editor-panel.component.scss'], encapsulation: ViewEncapsulation.None }) -export class ReleaseNotesPanelComponent implements OnInit { +export class EditorPanelComponent implements OnInit { @Input() disabled: boolean; @Input() - releaseNotes: string; + content: string; @Input() - isLatest: boolean; + title: string; @Input() - popover: TbPopoverComponent; + popover: TbPopoverComponent; @Output() - releaseNotesApplied = new EventEmitter(); - - title: string; + editorContentApplied = new EventEmitter(); - releaseNotesControl: FormControl; + editorControl: FormControl; tinyMceOptions: Record = { base_url: '/assets/tinymce', @@ -63,10 +61,9 @@ export class ReleaseNotesPanelComponent implements OnInit { } ngOnInit(): void { - this.releaseNotesControl = this.fb.control(this.releaseNotes); - this.title = this.isLatest ? 'mobile.latest-version-release-notes' : 'mobile.min-version-release-notes'; + this.editorControl = this.fb.control(this.content); if (this.disabled) { - this.releaseNotesControl.disable({emitEvent: false}); + this.editorControl.disable({emitEvent: false}); } } @@ -75,8 +72,8 @@ export class ReleaseNotesPanelComponent implements OnInit { } apply() { - if (this.releaseNotesControl.valid) { - this.releaseNotesApplied.emit(this.releaseNotesControl.value); + if (this.editorControl.valid) { + this.editorContentApplied.emit(this.editorControl.value); } } } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html index 618e0f65aa..4788d8265f 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html @@ -16,10 +16,11 @@ --> - + admin.mobile-app.mobile-app-qr-code-widget-settings +
@@ -51,110 +52,16 @@ formControlName="mobileAppBundleId">
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ + {{ 'admin.mobile-app.android' | translate }} + +
+
+ + {{ 'admin.mobile-app.ios' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts index 240d035ce0..2194d82759 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts @@ -35,70 +35,68 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; }) export class MobileQrCodeWidgetSettingsComponent extends PageComponent implements HasConfirmForm { - mobileAppSettingsForm: FormGroup; - mobileAppSettings: QrCodeSettings; - readonly badgePositionTranslationsMap = badgePositionTranslationsMap; readonly entityType = EntityType; + mobileAppSettingsForm = this.fb.group({ + useSystemSettings: [false], + useDefaultApp: [true], + mobileAppBundleId: [{value: null, disabled: true}, Validators.required], + androidEnabled: [true], + iosEnabled: [true], + qrCodeConfig: this.fb.group({ + showOnHomePage: [true], + badgeEnabled: [true], + badgePosition: [BadgePosition.RIGHT], + qrCodeLabelEnabled: [true], + qrCodeLabel: ['', [Validators.required, Validators.maxLength(50)]] + }) + }); + private authUser = getCurrentAuthUser(this.store); + private mobileAppSettings: QrCodeSettings; constructor(protected store: Store, private mobileAppService: MobileApplicationService, private fb: FormBuilder) { super(store); - this.buildMobileAppSettingsForm(); this.mobileAppService.getMobileAppSettings() .subscribe(settings => this.processMobileAppSettings(settings)); if (this.isTenantAdmin()) { - // this.mobileAppSettingsForm.get('useSystemSettings').valueChanges.pipe( - // takeUntilDestroyed() - // ).subscribe(value => { - // if (value) { - // this.mobileAppSettingsForm.get('androidConfig.enabled').disable(); - // this.mobileAppSettingsForm.get('iosConfig.enabled').disable(); - // this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').disable(); - // } else { - // this.mobileAppSettingsForm.get('androidConfig.enabled').enable(); - // this.mobileAppSettingsForm.get('iosConfig.enabled').enable(); - // this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').enable(); - // } - // }); + this.mobileAppSettingsForm.get('useSystemSettings').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(value => { + if (value) { + this.mobileAppSettingsForm.get('mobileAppBundleId').disable({emitEvent: false}); + this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').disable({emitEvent: false}); + } else { + this.mobileAppSettingsForm.get('mobileAppBundleId').enable({emitEvent: false}); + if (this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').value && + this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { + this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').enable({emitEvent: false}); + } + } + }); } this.mobileAppSettingsForm.get('useDefaultApp').valueChanges.pipe( takeUntilDestroyed() ).subscribe(value => { if (value) { this.mobileAppSettingsForm.get('mobileAppBundleId').disable({emitEvent: false}); - // this.mobileAppSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false}); - // this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false}); - // this.mobileAppSettingsForm.get('androidConfig.storeLink').disable({emitEvent: false}); - // this.mobileAppSettingsForm.get('iosConfig.appId').disable({emitEvent: false}); - // this.mobileAppSettingsForm.get('iosConfig.storeLink').disable({emitEvent: false}); } else { this.mobileAppSettingsForm.get('mobileAppBundleId').enable({emitEvent: false}); - - // if (this.mobileAppSettingsForm.get('androidConfig.enabled').value) { - // this.mobileAppSettingsForm.get('androidConfig.appPackage').enable({emitEvent: false}); - // this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').enable({emitEvent: false}); - // this.mobileAppSettingsForm.get('androidConfig.storeLink').enable({emitEvent: false}); - // } - // if (this.mobileAppSettingsForm.get('iosConfig.enabled').value) { - // this.mobileAppSettingsForm.get('iosConfig.appId').enable({emitEvent: false}); - // this.mobileAppSettingsForm.get('iosConfig.storeLink').enable({emitEvent: false}); - // } } }); - // this.mobileAppSettingsForm.get('androidConfig.enabled').valueChanges.pipe( - // takeUntilDestroyed() - // ).subscribe(value => { - // this.androidEnableChanged(value); - // }); - // this.mobileAppSettingsForm.get('iosConfig.enabled').valueChanges.pipe( - // takeUntilDestroyed() - // ).subscribe(value => { - // this.iosEnableChanged(value); - // }); + this.mobileAppSettingsForm.get('androidEnabled').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(() => { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').updateValueAndValidity({onlySelf: true}); + }); + this.mobileAppSettingsForm.get('iosEnabled').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(() => { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').updateValueAndValidity({onlySelf: true}); + }); this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').valueChanges.pipe( takeUntilDestroyed() ).subscribe(value => { @@ -115,13 +113,14 @@ export class MobileQrCodeWidgetSettingsComponent extends PageComponent implement takeUntilDestroyed() ).subscribe(value => { if (value) { - // if (this.mobileAppSettingsForm.get('androidConfig.enabled').value || this.mobileAppSettingsForm.get('iosConfig.enabled').value) { - // this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); - // this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable({emitEvent: false}); - // } else { - // this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); - // this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); - // } + const formValue = this.mobileAppSettingsForm.getRawValue(); + if (formValue.androidEnabled || formValue.iosEnabled) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable({emitEvent: false}); + } else { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); + } } else { this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); } @@ -141,32 +140,6 @@ export class MobileQrCodeWidgetSettingsComponent extends PageComponent implement return this.authUser.authority === Authority.TENANT_ADMIN; } - private buildMobileAppSettingsForm() { - this.mobileAppSettingsForm = this.fb.group({ - useSystemSettings: [false], - useDefaultApp: [true], - mobileAppBundleId: [{value: null, disabled: true}, Validators.required], - // androidConfig: this.fb.group({ - // enabled: [true], - // appPackage: [{value: '', disabled: true}, [Validators.required]], - // sha256CertFingerprints: [{value: '', disabled: true}, [Validators.required]], - // storeLink: ['', [Validators.required]] - // }), - // iosConfig: this.fb.group({ - // enabled: [true], - // appId: [{value: '', disabled: true}, [Validators.required]], - // storeLink: ['', [Validators.required]] - // }), - qrCodeConfig: this.fb.group({ - showOnHomePage: [true], - badgeEnabled: [true], - badgePosition: [BadgePosition.RIGHT], - qrCodeLabelEnabled: [true], - qrCodeLabel: ['', [Validators.required, Validators.maxLength(50)]] - }) - }); - } - private processMobileAppSettings(mobileAppSettings: QrCodeSettings): void { this.mobileAppSettings = {...mobileAppSettings}; if (!this.isTenantAdmin()) { @@ -175,34 +148,6 @@ export class MobileQrCodeWidgetSettingsComponent extends PageComponent implement this.mobileAppSettingsForm.reset(this.mobileAppSettings); } - // private androidEnableChanged(value: boolean): void { - // if (value) { - // if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { - // this.mobileAppSettingsForm.get('androidConfig.appPackage').enable({emitEvent: false}); - // this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').enable({emitEvent: false}); - // this.mobileAppSettingsForm.get('androidConfig.storeLink').enable({emitEvent: false}); - // } - // } else { - // this.mobileAppSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false}); - // this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false}); - // this.mobileAppSettingsForm.get('androidConfig.storeLink').disable({emitEvent: false}); - // } - // this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').updateValueAndValidity({onlySelf: true}); - // } - // - // private iosEnableChanged(value: boolean): void { - // if (value) { - // if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { - // this.mobileAppSettingsForm.get('iosConfig.appId').enable({emitEvent: false}); - // this.mobileAppSettingsForm.get('iosConfig.storeLink').enable({emitEvent: false}); - // } - // } else { - // this.mobileAppSettingsForm.get('iosConfig.appId').disable({emitEvent: false}); - // this.mobileAppSettingsForm.get('iosConfig.storeLink').disable({emitEvent: false}); - // } - // this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').updateValueAndValidity({onlySelf: true}); - // } - save(): void { const showOnHomePagePreviousValue = this.mobileAppSettings.qrCodeConfig.showOnHomePage; this.mobileAppSettings = {...this.mobileAppSettings, ...this.mobileAppSettingsForm.getRawValue()}; diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 1a681972bf..5170e8a160 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -187,7 +187,10 @@ export const HelpLinks = { gatewayInstall: `${helpBaseUrl}/docs/iot-gateway/install/docker-installation`, scada: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada`, scadaSymbolDev: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/symbols-dev-guide`, - scadaSymbolDevAnimation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolanimation` + scadaSymbolDevAnimation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolanimation`, + mobileApplication: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`, + mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`, + mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`, } }; /* eslint-enable max-len */ diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 8b570ac655..769ca2ce87 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -597,13 +597,13 @@ export const entityTypeResources = new Map Date: Fri, 1 Nov 2024 11:58:54 +0200 Subject: [PATCH 40/48] UI: Fixed typo --- .../home/pages/mobile/applications/applications.module.ts | 4 ++-- .../common/{comm-mobile.module.ts => common-mobile.module.ts} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename ui-ngx/src/app/modules/home/pages/mobile/common/{comm-mobile.module.ts => common-mobile.module.ts} (96%) diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts index fbdf4495eb..afcb8571cf 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts @@ -26,7 +26,7 @@ import { RemoveAppDialogComponent } from '@home/pages/mobile/applications/remove import { MobileAppConfigurationDialogComponent } from '@home/pages/mobile/applications/mobile-app-configuration-dialog.component'; -import { CommMobileModule } from '@home/pages/mobile/common/comm-mobile.module'; +import { CommonMobileModule } from '@home/pages/mobile/common/common-mobile.module'; @NgModule({ declarations: [ @@ -40,7 +40,7 @@ import { CommMobileModule } from '@home/pages/mobile/common/comm-mobile.module'; CommonModule, SharedModule, HomeComponentsModule, - CommMobileModule, + CommonMobileModule, ApplicationsRoutingModule, ], exports: [ diff --git a/ui-ngx/src/app/modules/home/pages/mobile/common/comm-mobile.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/common/common-mobile.module.ts similarity index 96% rename from ui-ngx/src/app/modules/home/pages/mobile/common/comm-mobile.module.ts rename to ui-ngx/src/app/modules/home/pages/mobile/common/common-mobile.module.ts index d0d54afcc9..4ea48b6cc1 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/common/comm-mobile.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/common/common-mobile.module.ts @@ -31,4 +31,4 @@ import { SharedModule } from '@shared/shared.module'; EditorPanelComponent ] }) -export class CommMobileModule {} +export class CommonMobileModule {} From 8491134eb0512f66e7c8258fcca450f41f417640 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 1 Nov 2024 15:15:06 +0200 Subject: [PATCH 41/48] UI: Fixed mobile qr code widget --- .../mobile-qr-code-widget-settings.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts index 2194d82759..ad1dcc2d2f 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts @@ -70,9 +70,11 @@ export class MobileQrCodeWidgetSettingsComponent extends PageComponent implement this.mobileAppSettingsForm.get('mobileAppBundleId').disable({emitEvent: false}); this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').disable({emitEvent: false}); } else { - this.mobileAppSettingsForm.get('mobileAppBundleId').enable({emitEvent: false}); - if (this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').value && - this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { + const formValue = this.mobileAppSettingsForm.value; + if (!formValue.useDefaultApp) { + this.mobileAppSettingsForm.get('mobileAppBundleId').enable({emitEvent: false}); + } + if (formValue.qrCodeConfig.qrCodeLabelEnabled && formValue.qrCodeConfig.showOnHomePage) { this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').enable({emitEvent: false}); } } From 7a60c0fbfdcc3869b4ccf9b072c71597b1d8fb82 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 4 Nov 2024 15:04:30 +0200 Subject: [PATCH 42/48] UI: Remove qr code widgets for tenant level --- ui-ngx/src/app/core/services/menu.models.ts | 3 +- ...bile-app-qr-code-basic-config.component.ts | 4 +-- ...ile-qr-code-widget-settings.component.html | 11 ++----- ...obile-qr-code-widget-settings.component.ts | 29 ------------------- .../app/shared/models/mobile-app.models.ts | 1 - 5 files changed, 5 insertions(+), 43 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 7dbb9614c8..f173ef1af5 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -820,8 +820,7 @@ const defaultUserMenuMap = new Map([ id: MenuId.mobile_center, pages: [ {id: MenuId.mobile_apps}, - {id: MenuId.mobile_bundles}, - {id: MenuId.mobile_qr_code_widget} + {id: MenuId.mobile_bundles} ] }, {id: MenuId.api_usage}, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts index f415e1d17c..d1ed782d03 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Injector } from '@angular/core'; +import { Component } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -42,8 +42,6 @@ export class MobileAppQrCodeBasicConfigComponent extends BasicWidgetConfigCompon constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, - private cd: ChangeDetectorRef, - private $injector: Injector, private fb: UntypedFormBuilder) { super(store, widgetConfigComponent); } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html index 4788d8265f..4005c60178 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html @@ -27,12 +27,7 @@
-
- - {{ 'admin.mobile-app.use-system-settings' | translate }} - -
-
+
admin.mobile-app.applications
@@ -63,7 +58,7 @@
-
+
admin.mobile-app.appearance-on-home-page
@@ -98,7 +93,7 @@ matTooltipClass="tb-error-tooltip" [matTooltip]="(mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').hasError('required') ? 'admin.mobile-app.label-required' : 'admin.mobile-app.label-max-length') | translate" - *ngIf="mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').touched && + *ngIf="!mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').untouched && mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').hasError('required') || mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').hasError('maxlength')" class="tb-error"> diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts index ad1dcc2d2f..3ddb15cd3b 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts @@ -24,8 +24,6 @@ import { MobileApplicationService } from '@core/http/mobile-application.service' import { BadgePosition, badgePositionTranslationsMap, QrCodeSettings } from '@shared/models/mobile-app.models'; import { ActionUpdateMobileQrCodeEnabled } from '@core/auth/auth.actions'; import { EntityType } from '@shared/models/entity-type.models'; -import { getCurrentAuthUser } from '@core/auth/auth.selectors'; -import { Authority } from '@shared/models/authority.enum'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ @@ -39,7 +37,6 @@ export class MobileQrCodeWidgetSettingsComponent extends PageComponent implement readonly entityType = EntityType; mobileAppSettingsForm = this.fb.group({ - useSystemSettings: [false], useDefaultApp: [true], mobileAppBundleId: [{value: null, disabled: true}, Validators.required], androidEnabled: [true], @@ -53,7 +50,6 @@ export class MobileQrCodeWidgetSettingsComponent extends PageComponent implement }) }); - private authUser = getCurrentAuthUser(this.store); private mobileAppSettings: QrCodeSettings; constructor(protected store: Store, @@ -62,24 +58,6 @@ export class MobileQrCodeWidgetSettingsComponent extends PageComponent implement super(store); this.mobileAppService.getMobileAppSettings() .subscribe(settings => this.processMobileAppSettings(settings)); - if (this.isTenantAdmin()) { - this.mobileAppSettingsForm.get('useSystemSettings').valueChanges.pipe( - takeUntilDestroyed() - ).subscribe(value => { - if (value) { - this.mobileAppSettingsForm.get('mobileAppBundleId').disable({emitEvent: false}); - this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').disable({emitEvent: false}); - } else { - const formValue = this.mobileAppSettingsForm.value; - if (!formValue.useDefaultApp) { - this.mobileAppSettingsForm.get('mobileAppBundleId').enable({emitEvent: false}); - } - if (formValue.qrCodeConfig.qrCodeLabelEnabled && formValue.qrCodeConfig.showOnHomePage) { - this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').enable({emitEvent: false}); - } - } - }); - } this.mobileAppSettingsForm.get('useDefaultApp').valueChanges.pipe( takeUntilDestroyed() ).subscribe(value => { @@ -138,15 +116,8 @@ export class MobileQrCodeWidgetSettingsComponent extends PageComponent implement }); } - public isTenantAdmin(): boolean { - return this.authUser.authority === Authority.TENANT_ADMIN; - } - private processMobileAppSettings(mobileAppSettings: QrCodeSettings): void { this.mobileAppSettings = {...mobileAppSettings}; - if (!this.isTenantAdmin()) { - this.mobileAppSettings.useSystemSettings = false; - } this.mobileAppSettingsForm.reset(this.mobileAppSettings); } diff --git a/ui-ngx/src/app/shared/models/mobile-app.models.ts b/ui-ngx/src/app/shared/models/mobile-app.models.ts index 167c4ca567..acf609a637 100644 --- a/ui-ngx/src/app/shared/models/mobile-app.models.ts +++ b/ui-ngx/src/app/shared/models/mobile-app.models.ts @@ -22,7 +22,6 @@ import { MobileAppBundleId } from '@shared/models/id/mobile-app-bundle-id'; import { deepClone, isNotEmptyStr } from '@core/utils'; export interface QrCodeSettings extends HasTenantId { - useSystemSettings: boolean; useDefaultApp: boolean; mobileAppBundleId: MobileAppBundleId androidEnabled: boolean; From 5fc412d728e1e9557a55a7365eb25147295a2528 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 5 Nov 2024 16:56:24 +0200 Subject: [PATCH 43/48] UI: Chang order mobile center pagss; Add validation in mobile app fields; Add mobile layout show hidden pages; Move config to bundle --- ui-ngx/src/app/core/services/menu.models.ts | 6 +- .../applications/applications.module.ts | 4 -- .../mobile-app-table-config.resolver.ts | 54 +--------------- .../applications/mobile-app.component.html | 23 ++++++- .../applications/mobile-app.component.ts | 23 ++++--- .../pages/mobile/bundes/bundles.module.ts | 5 ++ .../layout/mobile-layout.component.html | 1 + .../bundes/layout/mobile-layout.component.ts | 13 ++-- ...le-app-configuration-dialog.component.html | 0 ...le-app-configuration-dialog.component.scss | 4 +- ...bile-app-configuration-dialog.component.ts | 2 +- .../mobile-bundle-table-config.resolve.ts | 63 ++++++++++++++++++- .../pages/mobile/mobile-routing.module.ts | 2 +- .../app/shared/models/user-settings.models.ts | 2 +- .../assets/locale/locale.constant-en_US.json | 7 ++- 15 files changed, 124 insertions(+), 85 deletions(-) rename ui-ngx/src/app/modules/home/pages/mobile/{applications => bundes}/mobile-app-configuration-dialog.component.html (100%) rename ui-ngx/src/app/modules/home/pages/mobile/{applications => bundes}/mobile-app-configuration-dialog.component.scss (94%) rename ui-ngx/src/app/modules/home/pages/mobile/{applications => bundes}/mobile-app-configuration-dialog.component.ts (98%) diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index f173ef1af5..03eae14a56 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -723,8 +723,8 @@ const defaultUserMenuMap = new Map([ { id: MenuId.mobile_center, pages: [ - {id: MenuId.mobile_apps}, {id: MenuId.mobile_bundles}, + {id: MenuId.mobile_apps}, {id: MenuId.mobile_qr_code_widget} ] }, @@ -819,8 +819,8 @@ const defaultUserMenuMap = new Map([ { id: MenuId.mobile_center, pages: [ - {id: MenuId.mobile_apps}, - {id: MenuId.mobile_bundles} + {id: MenuId.mobile_bundles}, + {id: MenuId.mobile_apps} ] }, {id: MenuId.api_usage}, diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts index afcb8571cf..8e67f96a18 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts @@ -23,9 +23,6 @@ import { HomeComponentsModule } from '@home/components/home-components.module'; import { ApplicationsRoutingModule } from '@home/pages/mobile/applications/applications-routing.module'; import { MobileAppDialogComponent } from '@home/pages/mobile/applications/mobile-app-dialog.component'; import { RemoveAppDialogComponent } from '@home/pages/mobile/applications/remove-app-dialog.component'; -import { - MobileAppConfigurationDialogComponent -} from '@home/pages/mobile/applications/mobile-app-configuration-dialog.component'; import { CommonMobileModule } from '@home/pages/mobile/common/common-mobile.module'; @NgModule({ @@ -34,7 +31,6 @@ import { CommonMobileModule } from '@home/pages/mobile/common/common-mobile.modu MobileAppTableHeaderComponent, MobileAppDialogComponent, RemoveAppDialogComponent, - MobileAppConfigurationDialogComponent, ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts index d402c992e2..543ab89078 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts @@ -38,24 +38,11 @@ import { } from '@shared/models/mobile-app.models'; import { platformTypeTranslations } from '@shared/models/oauth2.models'; import { TruncatePipe } from '@shared/pipe/truncate.pipe'; -import { NotificationTemplate } from '@shared/models/notification.models'; -import { BaseData, HasId } from '@shared/models/base-data'; -import { - MobileBundleDialogComponent, - MobileBundleDialogData -} from '@home/pages/mobile/bundes/mobile-bundle-dialog.component'; import { MatDialog } from '@angular/material/dialog'; import { MobileAppDeleteDialogData, RemoveAppDialogComponent } from '@home/pages/mobile/applications/remove-app-dialog.component'; -import { - MobileAppConfigurationDialogComponent, MobileAppConfigurationDialogData -} from '@home/pages/mobile/applications/mobile-app-configuration-dialog.component'; -import { select, Store } from '@ngrx/store'; -import { selectUserSettingsProperty } from '@core/auth/auth.selectors'; -import { take } from 'rxjs/operators'; -import { AppState } from '@core/core.state'; @Injectable() export class MobileAppTableConfigResolver { @@ -66,8 +53,7 @@ export class MobileAppTableConfigResolver { private datePipe: DatePipe, private mobileAppService: MobileAppService, private truncatePipe: TruncatePipe, - private dialog: MatDialog, - private store: Store, + private dialog: MatDialog ) { this.config.selectionEnabled = false; this.config.entityType = EntityType.MOBILE_APP; @@ -130,18 +116,6 @@ export class MobileAppTableConfigResolver { this.config.saveEntity = (mobileApp) => this.mobileAppService.saveMobileApp(mobileApp); this.config.deleteEntity = id => this.mobileAppService.deleteMobileApp(id.id); - this.config.entityAdded = (mobileApp) => { - this.store.pipe(select(selectUserSettingsProperty( 'notDisplayConfigurationAfterAddMobileApp'))).pipe( - take(1) - ).subscribe((settings: boolean) => { - if(!settings) { - this.configurationApp(null, mobileApp, true); - } else { - this.config.updateData(); - } - }); - } - this.config.cellActionDescriptors = this.configureCellActions(); } @@ -151,12 +125,6 @@ export class MobileAppTableConfigResolver { private configureCellActions(): Array> { return [ - { - name: this.translate.instant('mobile.configuration-app'), - icon: 'code', - isEnabled: () => true, - onAction: ($event, entity) => this.configurationApp($event, entity) - }, { name: this.translate.instant('action.delete'), icon: 'delete', @@ -185,26 +153,6 @@ export class MobileAppTableConfigResolver { }); } - private configurationApp($event: Event, entity: MobileApp, afterAdd = false) { - if ($event) { - $event.stopPropagation(); - } - this.dialog.open(MobileAppConfigurationDialogComponent, { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - afterAdd, - appSecret: entity.appSecret - } - }).afterClosed() - .subscribe(() => { - if (afterAdd) { - this.config.updateData(); - } - }); - } - private mobileStatus(status: MobileAppStatus): string { const translateKey = mobileAppStatusTranslations.get(status); let backgroundColor = 'rgba(25, 128, 56, 0.06)'; diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html index 99460c8a6f..e74fb25b0b 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html @@ -35,7 +35,7 @@ {{ 'mobile.mobile-package-max-length' | translate }} - {{ 'mobile.mobile-package-spaces' | translate }} + {{ 'mobile.mobile-package-pattern' | translate }} @@ -84,9 +84,13 @@
mobile.version-information
- + mobile.min-version + + + {{ 'mobile.invalid-version-pattern' | translate }} +
- + mobile.latest-version + + + {{ 'mobile.invalid-version-pattern' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts index 4552cf8348..98195c9c84 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts @@ -64,20 +64,22 @@ export class MobileAppComponent extends EntityComponent { buildForm(entity: MobileApp): FormGroup { const form = this.fb.group({ pkgName: [entity?.pkgName ? entity.pkgName : '', [Validators.required, Validators.maxLength(255), - Validators.pattern(/^\S+$/)]], + Validators.pattern(/^[a-zA-Z][a-zA-Z\d_]*(?:\.[a-zA-Z][a-zA-Z\d_]*)+$/)]], platformType: [entity?.platformType ? entity.platformType : PlatformType.ANDROID], appSecret: [entity?.appSecret ? entity.appSecret : btoa(randomAlphanumeric(64)), [Validators.required, this.base64Format]], status: [entity?.status ? entity.status : MobileAppStatus.DRAFT], versionInfo: this.fb.group({ - minVersion: [entity?.versionInfo?.minVersion ? entity.versionInfo.minVersion : ''], + minVersion: [entity?.versionInfo?.minVersion ? entity.versionInfo.minVersion : '', Validators.pattern(/^\d+\.\d+\.\d+(-[a-zA-Z\d-.]+)?(\+[a-zA-Z\d-.]+)?$/)], minVersionReleaseNotes: [entity?.versionInfo?.minVersionReleaseNotes ? entity.versionInfo.minVersionReleaseNotes : ''], - latestVersion: [entity?.versionInfo?.latestVersion ? entity.versionInfo.latestVersion : ''], + latestVersion: [entity?.versionInfo?.latestVersion ? entity.versionInfo.latestVersion : '', Validators.pattern(/^\d+\.\d+\.\d+(-[a-zA-Z\d-.]+)?(\+[a-zA-Z\d-.]+)?$/)], latestVersionReleaseNotes: [entity?.versionInfo?.latestVersionReleaseNotes ? entity.versionInfo.latestVersionReleaseNotes : ''], }), storeInfo: this.fb.group({ - storeLink: [entity?.storeInfo?.storeLink ? entity.storeInfo.storeLink : ''], - sha256CertFingerprints: [entity?.storeInfo?.sha256CertFingerprints ? entity.storeInfo.sha256CertFingerprints : ''], - appId: [entity?.storeInfo?.appId ? entity.storeInfo.appId : ''], + storeLink: [entity?.storeInfo?.storeLink ? entity.storeInfo.storeLink : '', + Validators.pattern(/^https?:\/\/play\.google\.com\/store\/apps\/details\?id=[a-zA-Z0-9._]+$/)], + sha256CertFingerprints: [entity?.storeInfo?.sha256CertFingerprints ? entity.storeInfo.sha256CertFingerprints : '', + Validators.pattern(/^[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){1,31}$/)], + appId: [entity?.storeInfo?.appId ? entity.storeInfo.appId : '', Validators.pattern(/^\d{7,10}$/)], }), }); @@ -87,9 +89,11 @@ export class MobileAppComponent extends EntityComponent { if (value === PlatformType.ANDROID) { form.get('storeInfo.sha256CertFingerprints').enable({emitEvent: false}); form.get('storeInfo.appId').disable({emitEvent: false}); + form.get('storeInfo.storeLink').setValidators(Validators.pattern(/^https?:\/\/play\.google\.com\/store\/apps\/details\?id=[a-zA-Z0-9._]+$/)); } else if (value === PlatformType.IOS) { form.get('storeInfo.sha256CertFingerprints').disable({emitEvent: false}); form.get('storeInfo.appId').enable({emitEvent: false}); + form.get('storeInfo.storeLink').setValidators(Validators.pattern(/^https?:\/\/apps\.apple\.com\/[a-z]{2}\/app\/[\w-]+\/id\d{7,10}$/)); } form.get('storeInfo.storeLink').setValue('', {emitEvent: false}); }); @@ -99,12 +103,13 @@ export class MobileAppComponent extends EntityComponent { ).subscribe((value: MobileAppStatus) => { if (value !== MobileAppStatus.DRAFT) { form.get('storeInfo.storeLink').addValidators(Validators.required); - form.get('storeInfo.sha256CertFingerprints').addValidators(Validators.required); + form.get('storeInfo.sha256CertFingerprints') + .addValidators(Validators.required); form.get('storeInfo.appId').addValidators(Validators.required); } else { form.get('storeInfo.storeLink').clearValidators(); - form.get('storeInfo.sha256CertFingerprints').clearValidators(); - form.get('storeInfo.appId').clearValidators(); + form.get('storeInfo.sha256CertFingerprints').removeValidators(Validators.required); + form.get('storeInfo.appId').removeValidators(Validators.required); } form.get('storeInfo.storeLink').updateValueAndValidity({emitEvent: false}); form.get('storeInfo.sha256CertFingerprints').updateValueAndValidity({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/bundles.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/bundles.module.ts index ebbb5e534e..c0385a8dd5 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/bundles.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/bundles.module.ts @@ -27,6 +27,10 @@ import { AddMobilePageDialogComponent } from '@home/pages/mobile/bundes/layout/a import { CustomMobilePageComponent } from '@home/pages/mobile/bundes/layout/custom-mobile-page.component'; import { CustomMobilePagePanelComponent } from '@home/pages/mobile/bundes/layout/custom-mobile-page-panel.component'; import { DefaultMobilePagePanelComponent } from '@home/pages/mobile/bundes/layout/default-mobile-page-panel.component'; +import { + MobileAppConfigurationDialogComponent +} from '@home/pages/mobile/bundes/mobile-app-configuration-dialog.component'; + @NgModule({ declarations: [ @@ -38,6 +42,7 @@ import { DefaultMobilePagePanelComponent } from '@home/pages/mobile/bundes/layou CustomMobilePageComponent, CustomMobilePagePanelComponent, DefaultMobilePagePanelComponent, + MobileAppConfigurationDialogComponent, ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html index 705cc7031c..2bf38a94f1 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html @@ -19,6 +19,7 @@
mobile.pages + {{ 'mobile.show-hidden-pages' | translate }}
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts index df683cf572..8d2b0cc675 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts @@ -63,8 +63,8 @@ export class CustomMobilePageComponent implements ControlValueAccessor, Validato label: ['', Validators.required], type: [MobilePageType.DASHBOARD], dashboardId: ['', Validators.required], - url: [{value:'', disabled: true}, [Validators.required]], - path: [{value:'', disabled: true}, [Validators.required, Validators.pattern(/^((?!\s).)*$/)]] + url: [{value:'', disabled: true}, [Validators.required, Validators.pattern(/^(https?:\/\/)?(localhost|([\w\-]+\.)+[\w\-]+)(:\d+)?(\/[\w\-._~:\/?#[\]@!$&'()*+,;=%]*)?$/)]], + path: [{value:'', disabled: true}, [Validators.required, Validators.pattern(/^(\/[\w\-._~:\/?#[\]@!$&'()*+,;=%]*)?$/)]] }); private propagateChange = (_val: any) => {}; diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts index 308f69ae50..897710def9 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts @@ -53,7 +53,7 @@ export class MobileAppConfigurationDialogComponent extends DialogComponent