diff --git a/application/src/main/data/upgrade/3.8.1/schema_update.sql b/application/src/main/data/upgrade/3.8.1/schema_update.sql index 1084dd374f..8783ccf3e3 100644 --- a/application/src/main/data/upgrade/3.8.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.1/schema_update.sql @@ -24,3 +24,156 @@ UPDATE user_credentials c SET failed_login_attempts = (SELECT (additional_info:: UPDATE tb_user SET additional_info = (additional_info::jsonb - 'lastLoginTs' - 'failedLoginAttempts' - 'userCredentialsEnabled')::text WHERE additional_info IS NOT NULL AND additional_info != 'null'; + + +-- CREATE MOBILE APP BUNDLES FROM EXISTING APPS + +CREATE TABLE IF NOT EXISTS mobile_app_bundle ( + id uuid NOT NULL CONSTRAINT mobile_app_bundle_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid, + title varchar(255), + description varchar(1024), + android_app_id uuid UNIQUE, + ios_app_id uuid UNIQUE, + layout_config varchar(16384), + oauth2_enabled boolean, + CONSTRAINT fk_android_app_id FOREIGN KEY (android_app_id) REFERENCES mobile_app(id), + CONSTRAINT fk_ios_app_id FOREIGN KEY (ios_app_id) REFERENCES mobile_app(id) +); +CREATE INDEX IF NOT EXISTS mobile_app_bundle_tenant_id ON mobile_app_bundle(tenant_id); + +ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS platform_type varchar(32), + ADD COLUMN IF NOT EXISTS status varchar(32), + ADD COLUMN IF NOT EXISTS version_info varchar(100000), + ADD COLUMN IF NOT EXISTS store_info varchar(16384), + DROP CONSTRAINT IF EXISTS mobile_app_pkg_name_key, + DROP CONSTRAINT IF EXISTS mobile_app_unq_key; + +-- rename mobile_app_oauth2_client to mobile_app_bundle_oauth2_client +DO +$$ + BEGIN + -- in case of running the upgrade script a second time + IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'mobile_app_oauth2_client') THEN + ALTER TABLE mobile_app_oauth2_client RENAME TO mobile_app_bundle_oauth2_client; + ALTER TABLE mobile_app_bundle_oauth2_client DROP CONSTRAINT IF EXISTS fk_domain; + ALTER TABLE mobile_app_bundle_oauth2_client RENAME COLUMN mobile_app_id TO mobile_app_bundle_id; + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'fk_mobile_app_bundle_oauth2_client_bundle_id') THEN + ALTER TABLE mobile_app_bundle_oauth2_client ADD CONSTRAINT fk_mobile_app_bundle_oauth2_client_bundle_id + FOREIGN KEY (mobile_app_bundle_id) REFERENCES mobile_app_bundle(id) ON DELETE CASCADE; + END IF; + END IF; + END; +$$; + + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- duplicate each mobile app and create mobile app bundle for the pair of android and ios app +DO +$$ + DECLARE + generatedBundleId uuid; + iosAppId uuid; + mobileAppRecord RECORD; + BEGIN + -- in case of running the upgrade script a second time + IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'mobile_app' and column_name = 'oauth2_enabled') THEN + UPDATE mobile_app SET platform_type = 'ANDROID' WHERE platform_type IS NULL; + UPDATE mobile_app SET status = 'DRAFT' WHERE mobile_app.status IS NULL; + FOR mobileAppRecord IN SELECT * FROM mobile_app + LOOP + -- duplicate app for iOS platform type + iosAppId := uuid_generate_v4(); + INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, app_secret, platform_type, status) + VALUES (iosAppId, mobileAppRecord.created_time, mobileAppRecord.tenant_id, mobileAppRecord.pkg_name, mobileAppRecord.app_secret, 'IOS', mobileAppRecord.status) + ON CONFLICT DO NOTHING; + -- create bundle for android and iOS app + generatedBundleId := uuid_generate_v4(); + INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id, ios_app_id, oauth2_enabled) + VALUES (generatedBundleId, mobileAppRecord.created_time, mobileAppRecord.tenant_id, + mobileAppRecord.pkg_name || ' (autogenerated)', mobileAppRecord.id, iosAppId, mobileAppRecord.oauth2_enabled) + ON CONFLICT DO NOTHING; + UPDATE mobile_app_bundle_oauth2_client SET mobile_app_bundle_id = generatedBundleId WHERE mobile_app_bundle_id = mobileAppRecord.id; + END LOOP; + END IF; + ALTER TABLE mobile_app DROP COLUMN IF EXISTS oauth2_enabled; + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'mobile_app_pkg_name_platform_unq_key') THEN + ALTER TABLE mobile_app ADD CONSTRAINT mobile_app_pkg_name_platform_unq_key UNIQUE (pkg_name, platform_type); + END IF; + END; +$$; + +ALTER TABLE IF EXISTS mobile_app_settings RENAME TO qr_code_settings; +ALTER TABLE qr_code_settings ADD COLUMN IF NOT EXISTS mobile_app_bundle_id uuid, + ADD COLUMN IF NOT EXISTS android_enabled boolean, + ADD COLUMN IF NOT EXISTS ios_enabled boolean; + +-- migrate mobile apps from qr code settings to mobile_app, create mobile app bundle for the pair of apps +DO +$$ + DECLARE + androidPkgName varchar; + iosPkgName varchar; + androidAppId uuid; + iosAppId uuid; + generatedBundleId uuid; + qrCodeRecord RECORD; + BEGIN + -- in case of running the upgrade script a second time + IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'qr_code_settings' AND column_name = 'android_config') THEN + FOR qrCodeRecord IN SELECT * FROM qr_code_settings + LOOP + generatedBundleId := NULL; + -- migrate android config + IF (qrCodeRecord.android_config IS NOT NULL AND qrCodeRecord.android_config::jsonb -> 'appPackage' IS NOT NULL) THEN + androidPkgName := qrCodeRecord.android_config::jsonb ->> 'appPackage'; + SELECT id into androidAppId FROM mobile_app WHERE pkg_name = androidPkgName AND platform_type = 'ANDROID'; + IF androidAppId IS NULL THEN + androidAppId := uuid_generate_v4(); + INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, store_info) + VALUES (androidAppId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, + androidPkgName, 'ANDROID', 'DRAFT', qrCodeRecord.android_config::jsonb - 'appPackage' - 'enabled'); + generatedBundleId := uuid_generate_v4(); + INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id) + VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, androidPkgName || ' (autogenerated)', androidAppId); + UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId, + android_enabled = (qrCodeRecord.android_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id; + ELSE + UPDATE mobile_app SET store_info = qrCodeRecord.android_config::jsonb - 'appPackage' - 'enabled' WHERE id = androidAppId; + UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.android_app_id = androidAppId), + android_enabled = (qrCodeRecord.android_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id; + END IF; + END IF; + + -- migrate ios config + IF (qrCodeRecord.ios_config IS NOT NULL AND qrCodeRecord.ios_config::jsonb -> 'appId' IS NOT NULL) THEN + iosPkgName := substring(qrCodeRecord.ios_config::jsonb ->> 'appId', strpos(qrCodeRecord.ios_config::jsonb ->> 'appId', '.') + 1); + SELECT id INTO iosAppId FROM mobile_app WHERE pkg_name = iosPkgName AND platform_type = 'IOS'; + IF iosAppId IS NULL THEN + iosAppId := uuid_generate_v4(); + INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, store_info) + VALUES (iosAppId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, + iosPkgName, 'IOS', 'DRAFT', qrCodeRecord.ios_config); + IF generatedBundleId IS NULL THEN + generatedBundleId := uuid_generate_v4(); + INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, ios_app_id) + VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, iosPkgName || ' (autogenerated)', iosAppId); + UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId, + ios_enabled = (qrCodeRecord.ios_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id; + ELSE + UPDATE mobile_app_bundle SET ios_app_id = iosAppId WHERE id = generatedBundleId; + END IF; + ELSE + UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.ios_app_id = iosAppId), + ios_enabled = (qrCodeRecord.ios_config::jsonb -> 'enabled')::boolean WHERE id = qrCodeRecord.id; + UPDATE mobile_app SET store_info = qrCodeRecord.ios_config::jsonb - 'enabled' WHERE id = iosAppId; + END IF; + END IF; + END LOOP; + ALTER TABLE qr_code_settings RENAME CONSTRAINT mobile_app_settings_tenant_id_unq_key TO qr_code_settings_tenant_id_unq_key; + END IF; + ALTER TABLE qr_code_settings DROP COLUMN IF EXISTS android_config, DROP COLUMN IF EXISTS ios_config; + END; +$$; \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 5966716ed8..f6f2aef6f1 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -74,6 +74,7 @@ import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.dao.mobile.MobileAppBundleService; import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor; @@ -385,6 +386,10 @@ public class ActorSystemContext { @Getter private MobileAppService mobileAppService; + @Autowired + @Getter + private MobileAppBundleService mobileAppBundleService; + @Autowired @Getter private SlackService slackService; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index ec10402821..a62cb432da 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -89,6 +89,7 @@ import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.dao.mobile.MobileAppBundleService; import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.nosql.TbResultSetFuture; @@ -843,6 +844,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getMobileAppService(); } + @Override + public MobileAppBundleService getMobileAppBundleService() { + return mainCtx.getMobileAppBundleService(); + } + @Override public SlackService getSlackService() { return mainCtx.getSlackService(); 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 da7d6243fc..cc9c989cfa 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.HomeDashboardInfo; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.StringUtils; @@ -87,6 +88,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.MobileAppId; import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.OtaPackageId; @@ -101,7 +103,8 @@ import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetsBundleId; -import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; @@ -135,6 +138,7 @@ import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.mobile.MobileAppBundleService; import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.oauth2.OAuth2ClientService; @@ -198,6 +202,11 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @TbCoreComponent public abstract class BaseController { + protected static final String DASHBOARD_ID = "dashboardId"; + + protected static final String HOME_DASHBOARD_ID = "homeDashboardId"; + protected static final String HOME_DASHBOARD_HIDE_TOOLBAR = "homeDashboardHideToolbar"; + protected final Logger log = org.slf4j.LoggerFactory.getLogger(getClass()); /*Swagger UI description*/ @@ -262,6 +271,9 @@ public abstract class BaseController { @Autowired protected MobileAppService mobileAppService; + @Autowired + protected MobileAppBundleService mobileAppBundleService; + @Autowired protected OAuth2ConfigTemplateService oAuth2ConfigTemplateService; @@ -645,6 +657,9 @@ public abstract class BaseController { case MOBILE_APP: checkMobileAppId(new MobileAppId(entityId.getId()), operation); return; + case MOBILE_APP_BUNDLE: + checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation); + return; default: checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation); } @@ -833,6 +848,10 @@ public abstract class BaseController { return checkEntityId(mobileAppId, mobileAppService::findMobileAppById, operation); } + MobileAppBundle checkMobileAppBundleId(MobileAppBundleId mobileAppBundleId, Operation operation) throws ThingsboardException { + return checkEntityId(mobileAppBundleId, mobileAppBundleService::findMobileAppBundleById, operation); + } + protected I emptyId(EntityType entityType) { return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); } @@ -915,6 +934,37 @@ public abstract class BaseController { } } + protected HomeDashboardInfo getHomeDashboardInfo(SecurityUser securityUser, JsonNode additionalInfo) { + HomeDashboardInfo homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo); + if (homeDashboardInfo == null) { + if (securityUser.isCustomerUser()) { + Customer customer = customerService.findCustomerById(securityUser.getTenantId(), securityUser.getCustomerId()); + homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(customer.getAdditionalInfo()); + } + if (homeDashboardInfo == null) { + Tenant tenant = tenantService.findTenantById(securityUser.getTenantId()); + homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(tenant.getAdditionalInfo()); + } + } + return homeDashboardInfo; + } + + private HomeDashboardInfo extractHomeDashboardInfoFromAdditionalInfo(JsonNode additionalInfo) { + try { + if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) { + String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText(); + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + checkDashboardId(dashboardId, Operation.READ); + boolean hideDashboardToolbar = true; + if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) { + hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean(); + } + return new HomeDashboardInfo(dashboardId, hideDashboardToolbar); + } + } catch (Exception ignored) {} + return null; + } + protected MediaType parseMediaType(String contentType) { try { return MediaType.parseMediaType(contentType); 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..306444a23e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java @@ -0,0 +1,131 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.MobileAppBundleId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.config.annotations.ApiOperation; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.mobile.TbMobileAppBundleService; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import java.util.List; +import java.util.UUID; + +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@RequiredArgsConstructor +@Slf4j +public class MobileAppBundleController extends BaseController { + + private final TbMobileAppBundleService tbMobileAppBundleService; + + @ApiOperation(value = "Save Or update Mobile app bundle (saveMobileAppBundle)", + notes = "Create or update the Mobile app bundle that represents tha pair of ANDROID and IOS app and " + + "mobile settings like oauth2 clients, self-registration and layout configuration." + + "When creating mobile app bundle, platform generates Mobile App Bundle Id as " + UUID_WIKI_LINK + + "The newly created Mobile App Bundle Id will be present in the response. " + + "Referencing non-existing Mobile App Bundle Id will cause 'Not Found' error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PostMapping(value = "/mobile/bundle") + public MobileAppBundle saveMobileAppBundle( + @Parameter(description = "A JSON value representing the Mobile Application Bundle.", required = true) + @RequestBody @Valid MobileAppBundle mobileAppBundle, + @Parameter(description = "A list of oauth2 client ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam(name = "oauth2ClientIds", required = false) UUID[] ids) throws Exception { + mobileAppBundle.setTenantId(getTenantId()); + checkEntity(mobileAppBundle.getId(), mobileAppBundle, Resource.MOBILE_APP_BUNDLE); + return tbMobileAppBundleService.save(mobileAppBundle, getOAuth2ClientIds(ids), getCurrentUser()); + } + + @ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)", + notes = "Update oauth2 clients of the specified mobile app bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PutMapping(value = "/mobile/bundle/{id}/oauth2Clients") + public void updateOauth2Clients(@PathVariable UUID id, + @RequestBody UUID[] clientIds) throws ThingsboardException { + MobileAppBundleId mobileAppBundleId = new MobileAppBundleId(id); + MobileAppBundle mobileAppBundle = checkMobileAppBundleId(mobileAppBundleId, Operation.WRITE); + List oAuth2ClientIds = getOAuth2ClientIds(clientIds); + tbMobileAppBundleService.updateOauth2Clients(mobileAppBundle, oAuth2ClientIds, getCurrentUser()); + } + + @ApiOperation(value = "Get mobile app bundle infos (getTenantMobileAppBundleInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/mobile/bundle/infos") + public PageData getTenantMobileAppBundleInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Case-insensitive 'substring' filter based on app's name") + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return mobileAppBundleService.findMobileAppBundleInfosByTenantId(getTenantId(), pageLink); + } + + @ApiOperation(value = "Get mobile app bundle info by id (getMobileAppBundleInfoById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/mobile/bundle/info/{id}") + public MobileAppBundleInfo getMobileAppBundleInfoById(@PathVariable UUID id) throws ThingsboardException { + MobileAppBundleId mobileAppBundleId = new MobileAppBundleId(id); + return checkEntityId(mobileAppBundleId, mobileAppBundleService::findMobileAppBundleInfoById, Operation.READ); + } + + @ApiOperation(value = "Delete Mobile App Bundle by ID (deleteMobileAppBundle)", + notes = "Deletes Mobile App Bundle by ID. Referencing non-existing mobile app bundle Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @DeleteMapping(value = "/mobile/bundle/{id}") + public void deleteMobileAppBundle(@PathVariable UUID id) throws Exception { + MobileAppBundleId mobileAppBundleId = new MobileAppBundleId(id); + MobileAppBundle mobileAppBundle = checkMobileAppBundleId(mobileAppBundleId, Operation.DELETE); + tbMobileAppBundleService.delete(mobileAppBundle, getCurrentUser()); + } + +} 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..da206193fb 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java @@ -15,8 +15,9 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -26,33 +27,43 @@ import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.HomeDashboardInfo; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.Views; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.MobileAppId; -import org.thingsboard.server.common.data.id.OAuth2ClientId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.MobileApp; -import org.thingsboard.server.common.data.mobile.MobileAppInfo; +import org.thingsboard.server.common.data.mobile.LoginMobileInfo; +import org.thingsboard.server.common.data.mobile.UserMobileInfo; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppVersionInfo; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.layout.MobilePage; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.mobile.TbMobileAppService; +import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; +import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; @RestController @@ -64,70 +75,104 @@ public class MobileAppController extends BaseController { private final TbMobileAppService tbMobileAppService; + @ApiOperation(value = "Get mobile app login info (getLoginMobileInfo)") + @GetMapping(value = "/noauth/mobile") + public LoginMobileInfo getLoginMobileInfo(@Parameter(description = "Mobile application package name") + @RequestParam String pkgName, + @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) + @RequestParam PlatformType platform) { + List oauth2Clients = oAuth2ClientService.findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(pkgName, platform); + MobileApp mobileApp = mobileAppService.findMobileAppByPkgNameAndPlatformType(pkgName, platform); + return new LoginMobileInfo(oauth2Clients, mobileApp != null ? mobileApp.getVersionInfo() : null); + } + + @ApiOperation(value = "Get user mobile app basic info (getUserMobileInfo)", notes = AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/mobile") + public UserMobileInfo getUserMobileInfo(@Parameter(description = "Mobile application package name") + @RequestParam String pkgName, + @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) + @RequestParam PlatformType platform) throws ThingsboardException, JsonProcessingException { + SecurityUser securityUser = getCurrentUser(); + User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId()); + HomeDashboardInfo homeDashboardInfo = securityUser.isSystemAdmin() ? null : getHomeDashboardInfo(securityUser, user.getAdditionalInfo()); + MobileAppBundle mobileAppBundle = mobileAppBundleService.findMobileAppBundleByPkgNameAndPlatform(securityUser.getTenantId(), pkgName, platform); + return new UserMobileInfo(user, homeDashboardInfo, getVisiblePages(mobileAppBundle)); + } + + @ApiOperation(value = "Get mobile app version info (getMobileVersionInfo)") + @GetMapping(value = "/mobile/versionInfo") + public MobileAppVersionInfo getMobileVersionInfo(@Parameter(description = "Mobile application package name") + @RequestParam String pkgName, + @Parameter(description = "Platform type", schema = @Schema(allowableValues = {"ANDROID", "IOS"})) + @RequestParam PlatformType platform) { + MobileApp mobileApp = mobileAppService.findMobileAppByPkgNameAndPlatformType(pkgName, platform); + return mobileApp != null ? mobileApp.getVersionInfo() : null; + } + @ApiOperation(value = "Save Or update Mobile app (saveMobileApp)", notes = "Create or update the Mobile app. When creating mobile app, platform generates Mobile App Id as " + UUID_WIKI_LINK + "The newly created Mobile App Id will be present in the response. " + "Specify existing Mobile App Id to update the mobile app. " + "Referencing non-existing Mobile App Id will cause 'Not Found' error." + - "\n\nMobile app package name is unique for entire platform setup.\n\n" + SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @PostMapping(value = "/mobileApp") + "\n\nThe pair of mobile app package name and platform type is unique for entire platform setup.\n\n" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PostMapping(value = "/mobile/app") public MobileApp saveMobileApp( @Parameter(description = "A JSON value representing the Mobile Application.", required = true) - @RequestBody @Valid MobileApp mobileApp, - @Parameter(description = "A list of entity oauth2 client ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) - @RequestParam(name = "oauth2ClientIds", required = false) UUID[] ids) throws Exception { + @RequestBody @Valid MobileApp mobileApp) throws Exception { mobileApp.setTenantId(getTenantId()); checkEntity(mobileApp.getId(), mobileApp, Resource.MOBILE_APP); - return tbMobileAppService.save(mobileApp, getOAuth2ClientIds(ids), getCurrentUser()); + return tbMobileAppService.save(mobileApp, getCurrentUser()); } - @ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)", - notes = "Update oauth2 clients of the specified mobile app. ") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @PutMapping(value = "/mobileApp/{id}/oauth2Clients") - public void updateOauth2Clients(@PathVariable UUID id, - @RequestBody UUID[] clientIds) throws ThingsboardException { - MobileAppId mobileAppId = new MobileAppId(id); - MobileApp mobileApp = checkMobileAppId(mobileAppId, Operation.WRITE); - List oAuth2ClientIds = getOAuth2ClientIds(clientIds); - tbMobileAppService.updateOauth2Clients(mobileApp, oAuth2ClientIds, getCurrentUser()); - } - - @ApiOperation(value = "Get mobile app infos (getTenantMobileAppInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @GetMapping(value = "/mobileApp/infos") - public PageData getTenantMobileAppInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) - @RequestParam int pageSize, - @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) - @RequestParam int page, - @Parameter(description = "Case-insensitive 'substring' filter based on app's name") - @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION) - @RequestParam(required = false) String sortProperty, - @Parameter(description = SORT_ORDER_DESCRIPTION) - @RequestParam(required = false) String sortOrder) throws ThingsboardException { - accessControlService.checkPermission(getCurrentUser(), Resource.MOBILE_APP, Operation.READ); + @ApiOperation(value = "Get mobile app infos (getTenantMobileAppInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/mobile/app") + public PageData getTenantMobileApps(@Parameter(description = "Platform type: ANDROID or IOS") + @RequestParam(required = false) PlatformType platformType, + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Case-insensitive 'substring' filter based on app's name") + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return mobileAppService.findMobileAppInfosByTenantId(getTenantId(), pageLink); + return mobileAppService.findMobileAppsByTenantId(getTenantId(), platformType, pageLink); } - @ApiOperation(value = "Get mobile info by id (getMobileAppInfoById)", notes = SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @GetMapping(value = "/mobileApp/info/{id}") - public MobileAppInfo getMobileAppInfoById(@PathVariable UUID id) throws ThingsboardException { + @ApiOperation(value = "Get mobile info by id (getMobileAppInfoById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/mobile/app/{id}") + public MobileApp getMobileAppById(@PathVariable UUID id) throws ThingsboardException { MobileAppId mobileAppId = new MobileAppId(id); - return checkEntityId(mobileAppId, mobileAppService::findMobileAppInfoById, Operation.READ); + return checkEntityId(mobileAppId, mobileAppService::findMobileAppById, Operation.READ); } @ApiOperation(value = "Delete Mobile App by ID (deleteMobileApp)", - notes = "Deletes Mobile App by ID. Referencing non-existing mobile app Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('SYS_ADMIN')") - @DeleteMapping(value = "/mobileApp/{id}") + notes = "Deletes Mobile App by ID. Referencing non-existing mobile app Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @DeleteMapping(value = "/mobile/app/{id}") public void deleteMobileApp(@PathVariable UUID id) throws Exception { MobileAppId mobileAppId = new MobileAppId(id); MobileApp mobileApp = checkMobileAppId(mobileAppId, Operation.DELETE); tbMobileAppService.delete(mobileApp, getCurrentUser()); } + private JsonNode getVisiblePages(MobileAppBundle mobileAppBundle) throws JsonProcessingException { + if (mobileAppBundle != null && mobileAppBundle.getLayoutConfig() != null) { + List mobilePages = mobileAppBundle.getLayoutConfig().getPages() + .stream() + .filter(MobilePage::isVisible) + .collect(Collectors.toList()); + return JacksonUtil.toJsonNode(JacksonUtil.writeValueAsViewIgnoringNullFields(mobilePages, Views.Public.class)); + } else { + return JacksonUtil.newArrayNode(); + } + } + } 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..59c46fb1da 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, @@ -127,14 +128,13 @@ public class OAuth2Controller extends BaseController { @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CLIENT, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return oAuth2ClientService.findOAuth2ClientInfosByTenantId(getTenantId(), pageLink); } @ApiOperation(value = "Get OAuth2 Client infos By Ids (findTenantOAuth2ClientInfosByIds)", - notes = "Fetch OAuth2 Client info objects based on the provided ids. ") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + notes = "Fetch OAuth2 Client info objects based on the provided ids. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/oauth2/client/infos", params = {"clientIds"}) public List findTenantOAuth2ClientInfosByIds( @Parameter(description = "A list of oauth2 ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @@ -143,8 +143,8 @@ public class OAuth2Controller extends BaseController { return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds); } - @ApiOperation(value = "Get OAuth2 Client by id (getOAuth2ClientById)", notes = SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @ApiOperation(value = "Get OAuth2 Client by id (getOAuth2ClientById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/oauth2/client/{id}") public OAuth2Client getOAuth2ClientById(@PathVariable UUID id) throws ThingsboardException { OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(id); @@ -152,8 +152,8 @@ public class OAuth2Controller extends BaseController { } @ApiOperation(value = "Delete oauth2 client (deleteOauth2Client)", - notes = "Deletes the oauth2 client. Referencing non-existing oauth2 client Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('SYS_ADMIN')") + notes = "Deletes the oauth2 client. Referencing non-existing oauth2 client Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @DeleteMapping(value = "/oauth2/client/{id}") public void deleteOauth2Client(@PathVariable UUID id) throws Exception { OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(id); @@ -164,11 +164,10 @@ public class OAuth2Controller extends BaseController { @ApiOperation(value = "Get OAuth2 log in processing URL (getLoginProcessingUrl)", notes = "Returns the URL enclosed in " + "double quotes. After successful authentication with OAuth2 provider, it makes a redirect to this path so that the platform can do " + "further log in processing. This URL may be configured as 'security.oauth2.loginProcessingUrl' property in yml configuration file, or " + - "as 'SECURITY_OAUTH2_LOGIN_PROCESSING_URL' env variable. By default it is '/login/oauth2/code/'" + SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + "as 'SECURITY_OAUTH2_LOGIN_PROCESSING_URL' env variable. By default it is '/login/oauth2/code/'" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/oauth2/loginProcessingUrl") - public String getLoginProcessingUrl() throws ThingsboardException { - accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CLIENT, Operation.READ); + public String getLoginProcessingUrl() { return "\"" + oAuth2Configuration.getLoginProcessingUrl() + "\""; } 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 73% 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..3e3ffe9f20 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java @@ -31,13 +31,16 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.AndroidConfig; -import org.thingsboard.server.common.data.mobile.IosConfig; -import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.app.StoreInfo; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.config.annotations.ApiOperation; -import org.thingsboard.server.dao.mobile.MobileAppSettingsService; +import org.thingsboard.server.dao.mobile.MobileAppService; +import org.thingsboard.server.dao.mobile.QrCodeSettingService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.mobile.secret.MobileAppSecretService; import org.thingsboard.server.service.security.model.SecurityUser; @@ -48,13 +51,15 @@ import org.thingsboard.server.service.security.system.SystemSecurityService; import java.net.URI; import java.net.URISyntaxException; +import static org.thingsboard.server.common.data.oauth2.PlatformType.ANDROID; +import static org.thingsboard.server.common.data.oauth2.PlatformType.IOS; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; @RequiredArgsConstructor @RestController @TbCoreComponent -public class MobileApplicationController extends BaseController { +public class QrCodeSettingsController extends BaseController { @Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}") private int mobileSecretKeyTtl; @@ -89,15 +94,15 @@ public class MobileApplicationController extends BaseController { private final SystemSecurityService systemSecurityService; private final MobileAppSecretService mobileAppSecretService; - private final MobileAppSettingsService mobileAppSettingsService; + private final QrCodeSettingService qrCodeSettingService; @ApiOperation(value = "Get associated android applications (getAssetLinks)") @GetMapping(value = "/.well-known/assetlinks.json") public ResponseEntity getAssetLinks() { - MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); - AndroidConfig androidConfig = mobileAppSettings.getAndroidConfig(); - if (androidConfig != null && androidConfig.isEnabled() && androidConfig.getAppPackage() != null && androidConfig.getSha256CertFingerprints() != null) { - return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, androidConfig.getAppPackage(), androidConfig.getSha256CertFingerprints()))); + MobileApp mobileApp = qrCodeSettingService.findAppFromQrCodeSettings(TenantId.SYS_TENANT_ID, ANDROID); + StoreInfo storeInfo = mobileApp != null ? mobileApp.getStoreInfo() : null; + if (storeInfo != null && storeInfo.getSha256CertFingerprints() != null) { + return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, mobileApp.getPkgName(), storeInfo.getSha256CertFingerprints()))); } else { return ResponseEntity.notFound().build(); } @@ -106,10 +111,10 @@ public class MobileApplicationController extends BaseController { @ApiOperation(value = "Get associated ios applications (getAppleAppSiteAssociation)") @GetMapping(value = "/.well-known/apple-app-site-association") public ResponseEntity getAppleAppSiteAssociation() { - MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); - IosConfig iosConfig = mobileAppSettings.getIosConfig(); - if (iosConfig != null && iosConfig.isEnabled() && iosConfig.getAppId() != null) { - return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, iosConfig.getAppId()))); + MobileApp mobileApp = qrCodeSettingService.findAppFromQrCodeSettings(TenantId.SYS_TENANT_ID, IOS); + StoreInfo storeInfo = mobileApp != null ? mobileApp.getStoreInfo() : null; + if (storeInfo != null && storeInfo.getAppId() != null) { + return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, storeInfo.getAppId()))); } else { return ResponseEntity.notFound().build(); } @@ -118,40 +123,35 @@ public class MobileApplicationController extends BaseController { @ApiOperation(value = "Create Or Update the Mobile application settings (saveMobileAppSettings)", notes = "The request payload contains configuration for android/iOS applications and platform qr code widget settings." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @PostMapping(value = "/api/mobile/app/settings") - public MobileAppSettings saveMobileAppSettings(@Parameter(description = "A JSON value representing the mobile apps configuration") - @RequestBody MobileAppSettings mobileAppSettings) throws ThingsboardException { + @PostMapping(value = "/api/mobile/qr/settings") + public QrCodeSettings saveQrCodeSettings(@Parameter(description = "A JSON value representing the mobile apps configuration") + @RequestBody QrCodeSettings qrCodeSettings) throws ThingsboardException { SecurityUser currentUser = getCurrentUser(); accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.WRITE); - mobileAppSettings.setTenantId(getTenantId()); - return mobileAppSettingsService.saveMobileAppSettings(currentUser.getTenantId(), mobileAppSettings); + qrCodeSettings.setTenantId(getTenantId()); + return qrCodeSettingService.saveQrCodeSettings(currentUser.getTenantId(), qrCodeSettings); } @ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)", notes = "The response payload contains configuration for android/iOS applications and platform qr code widget settings." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/api/mobile/app/settings") - public MobileAppSettings getMobileAppSettings() throws ThingsboardException { + @GetMapping(value = "/api/mobile/qr/settings") + public QrCodeSettings getQrCodeSettings() throws ThingsboardException { SecurityUser currentUser = getCurrentUser(); accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ); - return mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); + return qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID); } @ApiOperation(value = "Get the deep link to the associated mobile application (getMobileAppDeepLink)", notes = "Fetch the url that takes user to linked mobile application " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/api/mobile/deepLink", produces = "text/plain") + @GetMapping(value = "/api/mobile/qr/deepLink", produces = "text/plain") public String getMobileAppDeepLink(HttpServletRequest request) throws ThingsboardException, URISyntaxException { String secret = mobileAppSecretService.generateMobileAppSecret(getCurrentUser()); String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, null, request); String platformDomain = new URI(baseUrl).getHost(); - MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); - String appDomain; - if (!mobileAppSettings.isUseDefaultApp()) { - appDomain = platformDomain; - } else { - appDomain = defaultAppDomain; - } + QrCodeSettings qrCodeSettings = qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID); + String appDomain = qrCodeSettings.isUseDefaultApp() ? defaultAppDomain : platformDomain; String deepLink = String.format(DEEP_LINK_PATTERN, appDomain, secret, mobileSecretKeyTtl); if (!appDomain.equals(platformDomain)) { deepLink = deepLink + "&host=" + baseUrl; @@ -170,15 +170,14 @@ public class MobileApplicationController extends BaseController { @GetMapping(value = "/api/noauth/qr") public ResponseEntity getApplicationRedirect(@RequestHeader(value = "User-Agent") String userAgent) { - MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); - boolean useDefaultApp = mobileAppSettings.isUseDefaultApp(); - String googlePlayLink = useDefaultApp ? mobileAppSettings.getDefaultGooglePlayLink() : mobileAppSettings.getAndroidConfig().getStoreLink(); - String appStoreLink = useDefaultApp ? mobileAppSettings.getDefaultAppStoreLink() : mobileAppSettings.getIosConfig().getStoreLink(); - if (userAgent.contains("Android")) { + QrCodeSettings qrCodeSettings = qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID); + if (userAgent.contains("Android") && qrCodeSettings.isAndroidEnabled()) { + String googlePlayLink = qrCodeSettings.getGooglePlayLink(); return ResponseEntity.status(HttpStatus.FOUND) .header("Location", googlePlayLink) .build(); - } else if (userAgent.contains("iPhone") || userAgent.contains("iPad")) { + } else if (userAgent.contains("iPhone") || userAgent.contains("iPad") && qrCodeSettings.isIosEnabled()) { + String appStoreLink = qrCodeSettings.getAppStoreLink(); return ResponseEntity.status(HttpStatus.FOUND) .header("Location", appStoreLink) .build(); 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..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,13 +34,13 @@ import org.thingsboard.server.common.data.SystemParams; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.MobileAppSettings; -import org.thingsboard.server.common.data.mobile.QRCodeConfig; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QRCodeConfig; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.dao.mobile.MobileAppSettingsService; +import org.thingsboard.server.dao.mobile.QrCodeSettingService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -77,7 +77,7 @@ public class SystemInfoController extends BaseController { private EntitiesVersionControlService versionControlService; @Autowired - private MobileAppSettingsService mobileAppSettingsService; + private QrCodeSettingService qrCodeSettingService; @PostConstruct public void init() { @@ -142,8 +142,8 @@ public class SystemInfoController extends BaseController { DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration(); systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize()); } - systemParams.setMobileQrEnabled(Optional.ofNullable(mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID)) - .map(MobileAppSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage) + systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) + .map(QrCodeSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage) .orElse(false)); return systemParams; } 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..5f9e673fe1 --- /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.bundle.MobileAppBundle; +import org.thingsboard.server.dao.mobile.MobileAppBundleService; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; + +import java.util.List; + +@Service +@AllArgsConstructor +public class DefaultTbMobileAppBundleService extends AbstractTbEntityService implements TbMobileAppBundleService { + + private final MobileAppBundleService mobileAppBundleService; + + @Override + public MobileAppBundle save(MobileAppBundle mobileAppBundle, List 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..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 { @@ -37,14 +33,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 +46,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..1efb73d0cf --- /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.bundle.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..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,16 +16,11 @@ package org.thingsboard.server.service.entitiy.mobile; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.id.OAuth2ClientId; -import org.thingsboard.server.common.data.mobile.MobileApp; - -import java.util.List; +import org.thingsboard.server.common.data.mobile.app.MobileApp; public interface TbMobileAppService { - MobileApp save(MobileApp mobileApp, List 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/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 52ef9b203f..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; @@ -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/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..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,8 +35,9 @@ public class SysAdminPermissions extends AbstractPermissions { put(Resource.USER, userPermissionChecker); put(Resource.WIDGETS_BUNDLE, systemEntityPermissionChecker); put(Resource.WIDGET_TYPE, systemEntityPermissionChecker); - put(Resource.OAUTH2_CLIENT, PermissionChecker.allowAllPermissionChecker); - put(Resource.MOBILE_APP, PermissionChecker.allowAllPermissionChecker); + put(Resource.OAUTH2_CLIENT, systemEntityPermissionChecker); + put(Resource.MOBILE_APP, systemEntityPermissionChecker); + put(Resource.MOBILE_APP_BUNDLE, systemEntityPermissionChecker); put(Resource.DOMAIN, PermissionChecker.allowAllPermissionChecker); put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, PermissionChecker.allowAllPermissionChecker); put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker); 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..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 @@ -51,6 +51,10 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker); put(Resource.NOTIFICATION, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ)); + put(Resource.OAUTH2_CLIENT, tenantEntityPermissionChecker); + put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, new PermissionChecker.GenericPermissionChecker(Operation.READ)); + put(Resource.MOBILE_APP, tenantEntityPermissionChecker); + put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 0c4648c7e6..6e71c64c86 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -609,8 +609,8 @@ cache: alarmTypes: timeToLiveInMinutes: "${CACHE_SPECS_ALARM_TYPES_TTL:60}" # Alarm types cache TTL maxSize: "${CACHE_SPECS_ALARM_TYPES_MAX_SIZE:10000}" # 0 means the cache is disabled - mobileAppSettings: - timeToLiveInMinutes: "${CACHE_SPECS_MOBILE_APP_SETTINGS_TTL:1440}" # Mobile application cache TTL + qrCodeSettings: + timeToLiveInMinutes: "${CACHE_SPECS_MOBILE_APP_SETTINGS_TTL:1440}" # Qr code settings cache TTL maxSize: "${CACHE_SPECS_MOBILE_APP_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled mobileSecretKey: timeToLiveInMinutes: "${CACHE_MOBILE_SECRET_KEY_TTL:2}" # QR secret key cache TTL 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..1030a6bd9c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/MobileAppBundleControllerTest.java @@ -0,0 +1,169 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppStatus; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +@DaoSqlTest +public class MobileAppBundleControllerTest extends AbstractControllerTest { + + static final TypeReference> 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); + androidApp = doPost("/api/mobile/app", androidApp, MobileApp.class); + + iosApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.ios.package", PlatformType.IOS); + iosApp = doPost("/api/mobile/app", iosApp, MobileApp.class); + } + + @After + public void tearDown() throws Exception { + PageData 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()); + } + + 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 testSaveMobileAppBundle() { + MobileAppBundle mobileAppBundle = new MobileAppBundle(); + mobileAppBundle.setTitle("Test bundle"); + mobileAppBundle.setAndroidAppId(androidApp.getId()); + mobileAppBundle.setIosAppId(iosApp.getId()); + + MobileAppBundle createdMobileAppBundle = doPost("/api/mobile/bundle", mobileAppBundle, MobileAppBundle.class); + assertThat(createdMobileAppBundle.getAndroidAppId()).isEqualTo(androidApp.getId()); + assertThat(createdMobileAppBundle.getIosAppId()).isEqualTo(iosApp.getId()); + } + + @Test + public void testSaveMobileAppBundleWithoutApps() throws Exception { + MobileAppBundle mobileAppBundle = new MobileAppBundle(); + mobileAppBundle.setTitle("Test bundle"); + + MobileAppBundle savedAppBundle = doPost("/api/mobile/bundle", mobileAppBundle, MobileAppBundle.class); + MobileAppBundleInfo retrievedMobileAppBundleInfo = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedAppBundle.getId().getId()); + assertThat(retrievedMobileAppBundleInfo).isEqualTo(new MobileAppBundleInfo(savedAppBundle, null, null, false, + Collections.emptyList())); + } + + + @Test + public void testUpdateMobileAppBundleOauth2Clients() throws Exception { + MobileAppBundle mobileAppBundle = new MobileAppBundle(); + mobileAppBundle.setTitle("Test bundle"); + mobileAppBundle.setAndroidAppId(androidApp.getId()); + mobileAppBundle.setIosAppId(iosApp.getId()); + + MobileAppBundle savedAppBundle = doPost("/api/mobile/bundle", mobileAppBundle, MobileAppBundle.class); + + OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); + OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); + + OAuth2Client oAuth2Client2 = createOauth2Client(TenantId.SYS_TENANT_ID, "test facebook client"); + OAuth2Client savedOAuth2Client2 = doPost("/api/oauth2/client", oAuth2Client2, OAuth2Client.class); + + doPut("/api/mobile/bundle/" + savedAppBundle.getId() + "/oauth2Clients", List.of(savedOAuth2Client.getId().getId(), savedOAuth2Client2.getId().getId())); + + MobileAppBundleInfo retrievedMobileAppBundleInfo = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedAppBundle.getId().getId()); + assertThat(retrievedMobileAppBundleInfo).isEqualTo(new MobileAppBundleInfo(savedAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), false, + Stream.of(new OAuth2ClientInfo(savedOAuth2Client), new OAuth2ClientInfo(savedOAuth2Client2)) + .sorted(Comparator.comparing(OAuth2ClientInfo::getTitle)).collect(Collectors.toList()) + )); + + doPut("/api/mobile/bundle/" + savedAppBundle.getId() + "/oauth2Clients", List.of(savedOAuth2Client2.getId().getId())); + MobileAppBundleInfo retrievedMobileAppInfo2 = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedAppBundle.getId().getId()); + assertThat(retrievedMobileAppInfo2).isEqualTo(new MobileAppBundleInfo(savedAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), false, List.of(new OAuth2ClientInfo(savedOAuth2Client2)))); + } + + @Test + public void testCreateMobileAppBundleWithOauth2Clients() throws Exception { + OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); + OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); + + MobileAppBundle mobileAppBundle = new MobileAppBundle(); + mobileAppBundle.setTitle("Test bundle"); + mobileAppBundle.setAndroidAppId(androidApp.getId()); + mobileAppBundle.setIosAppId(iosApp.getId()); + + MobileAppBundle savedMobileAppBundle = doPost("/api/mobile/bundle?oauth2ClientIds=" + savedOAuth2Client.getId().getId(), mobileAppBundle, MobileAppBundle.class); + + MobileAppBundleInfo retrievedMobileAppInfo = doGet("/api/mobile/bundle/info/{id}", MobileAppBundleInfo.class, savedMobileAppBundle.getId().getId()); + assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppBundleInfo(savedMobileAppBundle, androidApp.getPkgName(), iosApp.getPkgName(), false, List.of(new OAuth2ClientInfo(savedOAuth2Client)))); + } + + private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, PlatformType platformType) { + MobileApp mobileApp = new MobileApp(); + mobileApp.setTenantId(tenantId); + mobileApp.setStatus(MobileAppStatus.DRAFT); + mobileApp.setPkgName(mobileAppName); + mobileApp.setPlatformType(platformType); + mobileApp.setAppSecret(StringUtils.randomAlphanumeric(24)); + return mobileApp; + } + +} 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..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,18 +21,13 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.MobileApp; -import org.thingsboard.server.common.data.mobile.MobileAppInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2Client; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppStatus; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; -import java.util.Collections; -import java.util.List; - import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -41,9 +36,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest public class MobileAppControllerTest extends AbstractControllerTest { - static final TypeReference> 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,89 +46,64 @@ 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("my.test.package", PlatformType.ANDROID); + 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( "mobileApp.ce", PlatformType.ANDROID); mobileApp.setAppSecret("short"); - doPost("/api/mobileApp", mobileApp) + doPost("/api/mobile/app", mobileApp) .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString("appSecret must be at least 16 and max 2048 characters"))); } @Test - public void testUpdateMobileAppOauth2Clients() throws Exception { - MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.test.package", true); - MobileApp savedMobileApp = doPost("/api/mobileApp", mobileApp, MobileApp.class); - - OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); - OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); - - OAuth2Client oAuth2Client2 = createOauth2Client(TenantId.SYS_TENANT_ID, "test facebook client"); - OAuth2Client savedOAuth2Client2 = doPost("/api/oauth2/client", oAuth2Client2, OAuth2Client.class); - - doPut("/api/mobileApp/" + savedMobileApp.getId() + "/oauth2Clients", List.of(savedOAuth2Client.getId().getId(), savedOAuth2Client2.getId().getId())); - - MobileAppInfo retrievedMobileAppInfo = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId()); - assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppInfo(savedMobileApp, List.of(new OAuth2ClientInfo(savedOAuth2Client2), - new OAuth2ClientInfo(savedOAuth2Client)))); - - doPut("/api/mobileApp/" + savedMobileApp.getId() + "/oauth2Clients", List.of(savedOAuth2Client2.getId().getId())); - MobileAppInfo retrievedMobileAppInfo2 = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId()); - assertThat(retrievedMobileAppInfo2).isEqualTo(new MobileAppInfo(savedMobileApp, List.of(new OAuth2ClientInfo(savedOAuth2Client2)))); - } - - @Test - public void testCreateMobileAppWithOauth2Clients() throws Exception { - OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); - OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); - - MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.test.package", true); - MobileApp savedMobileApp = doPost("/api/mobileApp?oauth2ClientIds=" + savedOAuth2Client.getId().getId(), mobileApp, MobileApp.class); - - MobileAppInfo retrievedMobileAppInfo = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId()); - assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppInfo(savedMobileApp, List.of(new OAuth2ClientInfo(savedOAuth2Client)))); + public void testGetTenantAppsByPlatformTypeSaveMobileApp() throws Exception { + MobileApp androidApp = doPost("/api/mobile/app", validMobileApp("android.1", PlatformType.ANDROID), MobileApp.class); + MobileApp androidApp2 = doPost("/api/mobile/app", validMobileApp("android.2", PlatformType.ANDROID), MobileApp.class); + MobileApp iosApp = doPost("/api/mobile/app", validMobileApp("ios.1", PlatformType.IOS), MobileApp.class); + + PageData 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(TenantId tenantId, String mobileAppName, boolean oauth2Enabled) { - MobileApp MobileApp = new MobileApp(); - MobileApp.setTenantId(tenantId); - MobileApp.setPkgName(mobileAppName); - MobileApp.setAppSecret(StringUtils.randomAlphanumeric(24)); - MobileApp.setOauth2Enabled(oauth2Enabled); - return MobileApp; + private MobileApp validMobileApp(String mobileAppName, PlatformType platformType) { + MobileApp mobileApp = new MobileApp(); + mobileApp.setPkgName(mobileAppName); + mobileApp.setAppSecret(StringUtils.randomAlphanumeric(24)); + mobileApp.setPlatformType(platformType); + mobileApp.setStatus(MobileAppStatus.DRAFT); + return mobileApp; } } 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..73c02f7cb9 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/QrCodeSettingsControllerTest.java @@ -0,0 +1,262 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Value; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppStatus; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QRCodeConfig; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.app.StoreInfo; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.model.JwtPair; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +@DaoSqlTest +public class QrCodeSettingsControllerTest extends AbstractControllerTest { + + @Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}") + private int mobileSecretKeyTtl; + + static final TypeReference> 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( "my.android.package", PlatformType.ANDROID); + StoreInfo androidStoreInfo = StoreInfo.builder() + .sha256CertFingerprints(ANDROID_APP_SHA256) + .storeLink(ANDROID_STORE_LINK) + .build(); + androidApp.setStoreInfo(androidStoreInfo); + MobileApp savedAndroidApp = doPost("/api/mobile/app", androidApp, MobileApp.class); + + MobileApp iosApp = validMobileApp( "my.ios.package", PlatformType.IOS); + StoreInfo iosQrCodeConfig = StoreInfo.builder() + .appId(APPLE_APP_ID) + .storeLink(IOS_STORE_LINK) + .build(); + iosApp.setStoreInfo(iosQrCodeConfig); + MobileApp savedIosApp = doPost("/api/mobile/app", iosApp, MobileApp.class); + + mobileAppBundle = new MobileAppBundle(); + mobileAppBundle.setTitle("Test bundle"); + mobileAppBundle.setAndroidAppId(savedAndroidApp.getId()); + mobileAppBundle.setIosAppId(savedIosApp.getId()); + + mobileAppBundle = doPost("/api/mobile/bundle", mobileAppBundle, MobileAppBundle.class); + + QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class); + QRCodeConfig qrCodeConfig = new QRCodeConfig(); + qrCodeConfig.setQrCodeLabel(TEST_LABEL); + qrCodeSettings.setUseDefaultApp(true); + qrCodeSettings.setMobileAppBundleId(null); + qrCodeSettings.setQrCodeConfig(qrCodeConfig); + + doPost("/api/mobile/qr/settings", qrCodeSettings) + .andExpect(status().isOk()); + } + + @After + public void tearDown() throws Exception { + loginSysAdmin(); + PageData 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 + public void testSaveQrCodeSettings() throws Exception { + loginSysAdmin(); + QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class); + assertThat(qrCodeSettings.getQrCodeConfig().getQrCodeLabel()).isEqualTo(TEST_LABEL); + assertThat(qrCodeSettings.isUseDefaultApp()).isTrue(); + + qrCodeSettings.setUseDefaultApp(false); + qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId()); + + doPost("/api/mobile/qr/settings", qrCodeSettings) + .andExpect(status().isOk()); + + QrCodeSettings updatedQrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class); + assertThat(updatedQrCodeSettings.isUseDefaultApp()).isFalse(); + } + + @Test + public void testShouldNotSaveQrCodeSettingsWithoutRequiredConfig() throws Exception { + loginSysAdmin(); + QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class); + + qrCodeSettings.setUseDefaultApp(false); + qrCodeSettings.setQrCodeConfig(null); + qrCodeSettings.setMobileAppBundleId(null); + + doPost("/api/mobile/qr/settings", qrCodeSettings) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Validation error: qrCodeConfig must not be null"))); + + qrCodeSettings.setQrCodeConfig(QRCodeConfig.builder().showOnHomePage(false).build()); + doPost("/api/mobile/qr/settings", qrCodeSettings) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Mobile app bundle is required to use custom application!"))); + + qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId()); + doPost("/api/mobile/qr/settings", qrCodeSettings) + .andExpect(status().isOk()); + } + + @Test + public void testShouldSaveQrCodeSettingsForDefaultApp() throws Exception { + loginSysAdmin(); + QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class); + qrCodeSettings.setUseDefaultApp(true); + qrCodeSettings.setMobileAppBundleId(null); + + doPost("/api/mobile/qr/settings", qrCodeSettings) + .andExpect(status().isOk()); + } + + @Test + public void testGetApplicationAssociations() throws Exception { + loginSysAdmin(); + QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class); + qrCodeSettings.setUseDefaultApp(true); + qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId()); + doPost("/api/mobile/qr/settings", qrCodeSettings) + .andExpect(status().isOk()); + + JsonNode assetLinks = doGet("/.well-known/assetlinks.json", JsonNode.class); + assertThat(assetLinks.get(0).get("target").get("package_name").asText()).isEqualTo("my.android.package"); + assertThat(assetLinks.get(0).get("target").get("sha256_cert_fingerprints").get(0).asText()).isEqualTo(ANDROID_APP_SHA256); + + JsonNode appleAssociation = doGet("/.well-known/apple-app-site-association", JsonNode.class); + assertThat(appleAssociation.get("applinks").get("details").get(0).get("appID").asText()).isEqualTo(APPLE_APP_ID); + } + + @Test + public void testGetMobileDeepLink() throws Exception { + loginSysAdmin(); + String deepLink = doGet("/api/mobile/qr/deepLink", String.class); + + Pattern expectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)&host=([^&]+)\""); + Matcher parsedDeepLink = expectedPattern.matcher(deepLink); + assertThat(parsedDeepLink.matches()).isTrue(); + String appHost = parsedDeepLink.group(1); + String secret = parsedDeepLink.group(2); + String ttl = parsedDeepLink.group(3); + assertThat(appHost).isEqualTo("demo.thingsboard.io"); + assertThat(ttl).isEqualTo(String.valueOf(mobileSecretKeyTtl)); + + JwtPair jwtPair = doGet("/api/noauth/qr/" + secret, JwtPair.class); + assertThat(jwtPair).isNotNull(); + + loginTenantAdmin(); + String tenantDeepLink = doGet("/api/mobile/qr/deepLink", String.class); + Matcher tenantParsedDeepLink = expectedPattern.matcher(tenantDeepLink); + assertThat(tenantParsedDeepLink.matches()).isTrue(); + String tenantSecret = tenantParsedDeepLink.group(2); + + JwtPair tenantJwtPair = doGet("/api/noauth/qr/" + tenantSecret, JwtPair.class); + assertThat(tenantJwtPair).isNotNull(); + + loginCustomerUser(); + String customerDeepLink = doGet("/api/mobile/qr/deepLink", String.class); + Matcher customerParsedDeepLink = expectedPattern.matcher(customerDeepLink); + assertThat(customerParsedDeepLink.matches()).isTrue(); + String customerSecret = customerParsedDeepLink.group(2); + + JwtPair customerJwtPair = doGet("/api/noauth/qr/" + customerSecret, JwtPair.class); + assertThat(customerJwtPair).isNotNull(); + + // update mobile setting to use custom one + loginSysAdmin(); + QrCodeSettings qrCodeSettings = doGet("/api/mobile/qr/settings", QrCodeSettings.class); + qrCodeSettings.setUseDefaultApp(false); + qrCodeSettings.setMobileAppBundleId(mobileAppBundle.getId()); + doPost("/api/mobile/qr/settings", qrCodeSettings); + + String customAppDeepLink = doGet("/api/mobile/qr/deepLink", String.class); + Pattern customAppExpectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)\""); + Matcher customAppParsedDeepLink = customAppExpectedPattern.matcher(customAppDeepLink); + assertThat(customAppParsedDeepLink.matches()).isTrue(); + assertThat(customAppParsedDeepLink.group(1)).isEqualTo("localhost"); + + loginTenantAdmin(); + String tenantCustomAppDeepLink = doGet("/api/mobile/qr/deepLink", String.class); + Matcher tenantCustomAppParsedDeepLink = customAppExpectedPattern.matcher(tenantCustomAppDeepLink); + assertThat(tenantCustomAppParsedDeepLink.matches()).isTrue(); + assertThat(tenantCustomAppParsedDeepLink.group(1)).isEqualTo("localhost"); + + loginCustomerUser(); + String customerCustomAppDeepLink = doGet("/api/mobile/qr/deepLink", String.class); + Matcher customerCustomAppParsedDeepLink = customAppExpectedPattern.matcher(customerCustomAppDeepLink); + assertThat(customerCustomAppParsedDeepLink.matches()).isTrue(); + assertThat(customerCustomAppParsedDeepLink.group(1)).isEqualTo("localhost"); + } + + private MobileApp validMobileApp(String mobileAppName, PlatformType platformType) { + MobileApp mobileApp = new MobileApp(); + mobileApp.setTenantId(tenantId); + mobileApp.setStatus(MobileAppStatus.PUBLISHED); + mobileApp.setPkgName(mobileAppName); + mobileApp.setPlatformType(platformType); + mobileApp.setAppSecret(StringUtils.randomAlphanumeric(24)); + StoreInfo storeInfo = StoreInfo.builder() + .storeLink("https://play.google/test") + .sha256CertFingerprints(ANDROID_APP_SHA256) + .appId("test.app.id").build(); + mobileApp.setStoreInfo(storeInfo); + return mobileApp; + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index d3b1916def..c7f2e37211 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EventInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.EntityAlarm; @@ -45,6 +46,9 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.MobileAppBundleId; +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -54,7 +58,12 @@ import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppStatus; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; @@ -233,6 +242,27 @@ public class HousekeeperServiceTest extends AbstractControllerTest { tenantId = differentTenantId; createRelatedData(tenantId); + + MobileApp androidApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.android.package", PlatformType.ANDROID); + androidApp = doPost("/api/mobile/app", androidApp, MobileApp.class); + MobileAppId androidAppId = androidApp.getId(); + + MobileApp iosApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.ios.package", PlatformType.IOS); + iosApp = doPost("/api/mobile/app", iosApp, MobileApp.class); + MobileAppId iosAppId = androidApp.getId(); + + OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); + OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); + OAuth2ClientId oAuth2ClientId = savedOAuth2Client.getId(); + + MobileAppBundle mobileAppBundle = new MobileAppBundle(); + mobileAppBundle.setTitle("Test bundle"); + mobileAppBundle.setAndroidAppId(androidApp.getId()); + mobileAppBundle.setIosAppId(iosApp.getId()); + + MobileAppBundle savedAppBundle = doPost("/api/mobile/bundle?oauth2ClientIds=" + savedOAuth2Client.getId().getId(), mobileAppBundle, MobileAppBundle.class); + MobileAppBundleId appBundleId = savedAppBundle.getId(); + createDifferentTenantCustomer(); createRelatedData(differentTenantCustomerId); loginDifferentTenant(); @@ -279,6 +309,10 @@ public class HousekeeperServiceTest extends AbstractControllerTest { verifyNoRelatedData(userId); verifyNoRelatedData(differentTenantCustomerId); verifyNoRelatedData(tenantApiUsageState.getId()); + verifyNoRelatedData(androidAppId); + verifyNoRelatedData(iosAppId); + verifyNoRelatedData(oAuth2ClientId); + verifyNoRelatedData(appBundleId); verifyNoRelatedData(tenantId); }); } @@ -483,4 +517,14 @@ public class HousekeeperServiceTest extends AbstractControllerTest { return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainId); } + private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, PlatformType platformType) { + MobileApp mobileApp = new MobileApp(); + mobileApp.setTenantId(tenantId); + mobileApp.setStatus(MobileAppStatus.DRAFT); + mobileApp.setPkgName(mobileAppName); + mobileApp.setPlatformType(platformType); + mobileApp.setAppSecret(StringUtils.randomAlphanumeric(24)); + return mobileApp; + } + } 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..35943e72c7 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleService.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.mobile; + +import org.thingsboard.server.common.data.id.MobileAppBundleId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.EntityDaoService; + +import java.util.List; + +public interface MobileAppBundleService extends EntityDaoService { + + MobileAppBundle saveMobileAppBundle(TenantId tenantId, MobileAppBundle mobileAppBundle); + + void updateOauth2Clients(TenantId tenantId, MobileAppBundleId mobileAppBundleId, List oAuth2ClientIds); + + MobileAppBundle findMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId); + + PageData findMobileAppBundleInfosByTenantId(TenantId tenantId, PageLink pageLink); + + MobileAppBundleInfo findMobileAppBundleInfoById(TenantId tenantId, MobileAppBundleId mobileAppBundleId); + + MobileAppBundle findMobileAppBundleByPkgNameAndPlatform(TenantId tenantId, String pkgName, PlatformType platform); + + void deleteMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId); + +} 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..570531eb86 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,30 +15,27 @@ */ package org.thingsboard.server.dao.mobile; +import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.MobileAppId; -import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.MobileApp; -import org.thingsboard.server.common.data.mobile.MobileAppInfo; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.EntityDaoService; -import java.util.List; - public interface MobileAppService extends EntityDaoService { MobileApp saveMobileApp(TenantId tenantId, MobileApp mobileApp); - void deleteMobileAppById(TenantId tenantId, MobileAppId mobileAppId); - MobileApp findMobileAppById(TenantId tenantId, MobileAppId mobileAppId); - PageData findMobileAppInfosByTenantId(TenantId tenantId, PageLink pageLink); + PageData findMobileAppsByTenantId(TenantId tenantId, PlatformType platformType, PageLink pageLink); - MobileAppInfo findMobileAppInfoById(TenantId tenantId, MobileAppId mobileAppId); + MobileApp findByBundleIdAndPlatformType(TenantId tenantId, MobileAppBundleId mobileAppBundleId, PlatformType platformType); - void updateOauth2Clients(TenantId tenantId, MobileAppId mobileAppId, List oAuth2ClientIds); + 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/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..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 @@ -60,7 +60,8 @@ public enum EntityType { QUEUE_STATS(34), OAUTH2_CLIENT(35), DOMAIN(36), - MOBILE_APP(37); + MOBILE_APP(37), + MOBILE_APP_BUNDLE(38); @Getter private final int protoNumber; // Corresponds to EntityTypeProto 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..9694ad56ce --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Views.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public class Views { + public static class Public {} + public static class Private extends Public {} +} 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/LoginMobileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/LoginMobileInfo.java new file mode 100644 index 0000000000..059dc17295 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/LoginMobileInfo.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile; + +import org.thingsboard.server.common.data.mobile.app.MobileAppVersionInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; + +import java.util.List; + +public record LoginMobileInfo(List oAuth2ClientLoginInfos, MobileAppVersionInfo versionInfo) { +} 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..f7a4033b16 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,10 @@ */ package org.thingsboard.server.common.data.mobile; -import lombok.Data; +import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.HomeDashboardInfo; +import org.thingsboard.server.common.data.User; -import java.util.Map; - -@Data -public class UserMobileInfo { - - private Map sessions; +public record UserMobileInfo(User user, HomeDashboardInfo homeDashboardInfo, JsonNode pages) { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileSessionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileSessionInfo.java new file mode 100644 index 0000000000..414059a92c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileSessionInfo.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile; + +import lombok.Data; + +import java.util.Map; + +@Data +public class UserMobileSessionInfo { + + private Map sessions; + +} 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 72% 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 ad207e5635..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,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.app; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -27,6 +29,7 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.MobileAppId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.validation.Length; @EqualsAndHashCode(callSuper = true) @@ -44,8 +47,18 @@ public class MobileApp extends BaseData implements HasTenantId, Has @NotEmpty @Length(fieldName = "appSecret", min = 16, max = 2048, message = "must be at least 16 and max 2048 characters") private String appSecret; - @Schema(description = "Whether OAuth2 settings are enabled or not") - private boolean oauth2Enabled; + @Schema(description = "Application platform type: ANDROID or IOS", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + private PlatformType platformType; + @Schema(description = "Application status: PUBLISHED, DEPRECATED, SUSPENDED, DRAFT", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + private MobileAppStatus status; + @Schema(description = "Application version info") + @Valid + private MobileAppVersionInfo versionInfo; + @Schema(description = "Application store information") + @Valid + private StoreInfo storeInfo; public MobileApp() { super(); @@ -60,7 +73,10 @@ public class MobileApp extends BaseData implements HasTenantId, Has this.tenantId = mobile.tenantId; this.pkgName = mobile.pkgName; this.appSecret = mobile.appSecret; - this.oauth2Enabled = mobile.oauth2Enabled; + this.platformType = mobile.platformType; + this.status = mobile.status; + this.versionInfo = mobile.versionInfo; + this.storeInfo = mobile.storeInfo; } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppStatus.java new file mode 100644 index 0000000000..062c3f8b6b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppStatus.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile.app; + +public enum MobileAppStatus { + + DRAFT, + PUBLISHED, + DEPRECATED, + SUSPENDED + +} 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 new file mode 100644 index 0000000000..2ed6e962ec --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileAppVersionInfo.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile.app; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.validation.Length; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class MobileAppVersionInfo { + + @Schema(description = "Minimum supported version") + @Length(fieldName = "minVersion", max = 20) + private String minVersion; + + @Schema(description = "Release notes of minimum supported version") + @Length(fieldName = "minVersionReleaseNotes", max = 40000) + private String minVersionReleaseNotes; + + @Schema(description = "Latest supported version") + @Length(fieldName = "latestVersion", max = 20) + private String latestVersion; + + @Schema(description = "Release notes of latest supported version") + @Length(fieldName = "latestVersionReleaseNotes", max = 40000) + private String latestVersionReleaseNotes; + +} 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/app/StoreInfo.java similarity index 75% 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/app/StoreInfo.java index 7d40dfe805..9903302592 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/app/StoreInfo.java @@ -13,26 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.app; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.validation.NoXss; @Data @Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -public class IosConfig { +public class StoreInfo { - private boolean enabled; @NoXss private String appId; @NoXss + private String sha256CertFingerprints; + @NoXss private String storeLink; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java new file mode 100644 index 0000000000..8f72d1ffa2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java @@ -0,0 +1,83 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile.bundle; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.MobileAppBundleId; +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig; +import org.thingsboard.server.common.data.validation.Length; + +@EqualsAndHashCode(callSuper = true) +@Data +@ToString +public class MobileAppBundle extends BaseData 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/bundle/MobileAppBundleInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundleInfo.java new file mode 100644 index 0000000000..b88d5decc9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundleInfo.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile.bundle; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.id.MobileAppBundleId; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +@Schema +public class MobileAppBundleInfo extends MobileAppBundle { + + @Schema(description = "Android package name") + private String androidPkgName; + @Schema(description = "IOS package name") + private String iosPkgName; + @Schema(description = "List of available oauth2 clients") + private List oauth2ClientInfos; + @Schema(description = "Indicates if qr code is available for bundle") + private boolean qrCodeEnabled; + + public MobileAppBundleInfo(MobileAppBundle mobileApp, String androidPkgName, String iosPkgName, boolean qrCodeEnabled) { + super(mobileApp); + this.androidPkgName = androidPkgName; + this.iosPkgName = iosPkgName; + this.qrCodeEnabled = qrCodeEnabled; + } + + public MobileAppBundleInfo(MobileAppBundle mobileApp, String androidPkgName, String iosPkgName, boolean qrCodeEnabled, List oauth2ClientInfos) { + super(mobileApp); + this.androidPkgName = androidPkgName; + this.iosPkgName = iosPkgName; + this.qrCodeEnabled = qrCodeEnabled; + this.oauth2ClientInfos = oauth2ClientInfos; + } + + public MobileAppBundleInfo() { + super(); + } + + 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/bundle/MobileAppBundleOauth2Client.java similarity index 80% 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/bundle/MobileAppBundleOauth2Client.java index 2be75db92f..1309adc9f3 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/bundle/MobileAppBundleOauth2Client.java @@ -13,20 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.bundle; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.OAuth2ClientId; @Data @NoArgsConstructor @AllArgsConstructor -public class MobileAppOauth2Client { +public class MobileAppBundleOauth2Client { - private MobileAppId mobileAppId; + private MobileAppBundleId mobileAppBundleId; private OAuth2ClientId oAuth2ClientId; } 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..9a97b737f0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile.layout; + +import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.thingsboard.server.common.data.Views; + +@Data +public abstract class AbstractMobilePage implements MobilePage { + + @Schema(description = "Page label", example = "Air quality") + @JsonView(Views.Public.class) + protected String label; + @Schema(description = "Indicates if page is visible", example = "true", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonView(Views.Private.class) + protected boolean visible; + @Schema(description = "URL of the page icon", example = "home_icon") + @JsonView(Views.Public.class) + protected String icon; +} 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 new file mode 100644 index 0000000000..d2e4db347c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile.layout; + +import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.Views; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class CustomMobilePage extends AbstractMobilePage { + + @Schema(description = "Path to custom page", example = "/alarmDetails/868c7083-032d-4f52-b8b4-7859aebb6a4e") + @JsonView(Views.Public.class) + private String path; + + @Override + public MobilePageType getType() { + return MobilePageType.CUSTOM; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java new file mode 100644 index 0000000000..8adb50c140 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile.layout; + +import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.Views; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class DashboardPage extends AbstractMobilePage { + + @Schema(description = "Dashboard id", example = "784f394c-42b6-435a-983c-b7beff2784f9") + @JsonView(Views.Public.class) + private String dashboardId; + + @Override + public MobilePageType getType() { + return MobilePageType.DASHBOARD; + } +} 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..c4504ee1e2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile.layout; + +import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.Views; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class DefaultMobilePage extends AbstractMobilePage { + + @Schema(description = "Identifier for default page", example = "HOME") + @JsonView(Views.Public.class) + private DefaultPageId id; + + @Override + public MobilePageType getType() { + return MobilePageType.DEFAULT; + } + +} 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 new file mode 100644 index 0000000000..3409fec793 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultPageId.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile.layout; + +public enum DefaultPageId { + + HOME, + ALARMS, + DEVICES, + CUSTOMERS, + ASSETS, + AUDIT_LOGS, + NOTIFICATIONS, + DEVICE_LIST, + DASHBOARDS +} 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/layout/MobileLayoutConfig.java similarity index 64% 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/layout/MobileLayoutConfig.java index d670382462..5add86dbef 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/layout/MobileLayoutConfig.java @@ -13,28 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.layout; +import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; -import org.thingsboard.server.common.data.validation.NoXss; +import org.thingsboard.server.common.data.Views; + +import java.util.ArrayList; +import java.util.List; @Data @Builder @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode -public class AndroidConfig { +public class MobileLayoutConfig { - private boolean enabled; - @NoXss - private String appPackage; - @NoXss - private String sha256CertFingerprints; - @NoXss - private String storeLink; + @Schema(description = "List of pages") + @JsonView(Views.Public.class) + @Valid + private List 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..fd909202e1 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile.layout; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonView; +import org.thingsboard.server.common.data.Views; + +import java.io.Serializable; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DefaultMobilePage.class, name = "DEFAULT"), + @JsonSubTypes.Type(value = DashboardPage.class, name = "DASHBOARD"), + @JsonSubTypes.Type(value = WebViewPage.class, name = "WEB_VIEW"), + @JsonSubTypes.Type(value = CustomMobilePage.class, name = "CUSTOM") +}) +public interface MobilePage extends Serializable { + + @JsonView(Views.Private.class) + MobilePageType getType(); + + @JsonView(Views.Private.class) + boolean isVisible(); + +} 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..afb9ca7798 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile.layout; + +import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.Views; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class WebViewPage extends AbstractMobilePage { + + @Schema(description = "Url", example = "/url") + @JsonView(Views.Public.class) + private String url; + + @Override + public MobilePageType getType() { + return MobilePageType.WEB_VIEW; + } +} 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/MobileAppSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/QrCodeSettings.java similarity index 59% 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/QrCodeSettings.java index 31b2029bfc..9742d74ba4 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/QrCodeSettings.java @@ -13,49 +13,52 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.common.data.mobile.qrCodeSettings; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.id.MobileAppSettingsId; +import org.thingsboard.server.common.data.id.MobileAppBundleId; +import org.thingsboard.server.common.data.id.QrCodeSettingsId; import org.thingsboard.server.common.data.id.TenantId; @Schema @Data @EqualsAndHashCode(callSuper = true) -public class MobileAppSettings extends BaseData implements HasTenantId { +public class QrCodeSettings extends BaseData implements HasTenantId { private static final long serialVersionUID = 2628323657987010348L; @Schema(description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY) private TenantId tenantId; - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Type of application: true means use default Thingsboard app", example = "true") + @Schema(description = "Use settings from system level", example = "true") + private boolean useSystemSettings; + @Schema(description = "Type of application: true means use default Thingsboard app", example = "true") private boolean useDefaultApp; - @Valid - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Android mobile app configuration.") - private AndroidConfig androidConfig; - @Valid - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Ios mobile app configuration.") - private IosConfig iosConfig; - @Valid + @Schema(description = "Mobile app bundle.") + private MobileAppBundleId mobileAppBundleId; @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "QR code config configuration.") + @Valid + @NotNull private QRCodeConfig qrCodeConfig; - + @Schema(description = "Indicates if google play link is available", example = "true") + private boolean androidEnabled; + @Schema(description = "Indicates if apple store link is available", example = "true") + private boolean iosEnabled; @JsonProperty(access = JsonProperty.Access.READ_ONLY) - private String defaultGooglePlayLink; - + private String googlePlayLink; @JsonProperty(access = JsonProperty.Access.READ_ONLY) - private String defaultAppStoreLink; + private String appStoreLink; - public MobileAppSettings() { + public QrCodeSettings() { } - public MobileAppSettings(MobileAppSettingsId id) { + public QrCodeSettings(QrCodeSettingsId id) { super(id); } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index f84c7f7c9b..3052b30b98 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -58,6 +58,7 @@ enum EntityTypeProto { OAUTH2_CLIENT = 35; DOMAIN = 36; MOBILE_APP = 37; + MOBILE_APP_BUNDLE = 38; } /** 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); 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..d100536bf5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleDao.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.mobile; + +import org.thingsboard.server.common.data.id.MobileAppBundleId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleOauth2Client; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.Dao; + +import java.util.List; + +public interface MobileAppBundleDao extends Dao { + + PageData findInfosByTenantId(TenantId tenantId, PageLink pageLink); + + MobileAppBundleInfo findInfoById(TenantId tenantId, MobileAppBundleId mobileAppBundleId); + + List findOauth2ClientsByMobileAppBundleId(TenantId tenantId, MobileAppBundleId mobileAppBundleId); + + void addOauth2Client(TenantId tenantId, MobileAppBundleOauth2Client mobileAppBundleOauth2Client); + + void removeOauth2Client(TenantId tenantId, MobileAppBundleOauth2Client mobileAppBundleOauth2Client); + + MobileAppBundle findByPkgNameAndPlatform(TenantId tenantId, String pkgName, PlatformType platform); + + void deleteByTenantId(TenantId tenantId); + +} 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..4997ad8d30 --- /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.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleOauth2Client; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import org.thingsboard.server.dao.oauth2.OAuth2ClientDao; +import org.thingsboard.server.dao.service.DataValidator; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.thingsboard.server.dao.service.Validator.checkNotNull; + +@Slf4j +@Service +public class MobileAppBundleServiceImpl extends AbstractEntityService implements MobileAppBundleService { + + private static final String PLATFORM_TYPE_IS_REQUIRED = "Platform type is required if package name is specified"; + + @Autowired + private OAuth2ClientDao oauth2ClientDao; + @Autowired + private MobileAppBundleDao mobileAppBundleDao; + @Autowired + private DataValidator mobileAppBundleDataValidator; + + + @Override + public MobileAppBundle saveMobileAppBundle(TenantId tenantId, MobileAppBundle mobileAppBundle) { + log.trace("Executing saveMobileAppBundle [{}]", mobileAppBundle); + mobileAppBundleDataValidator.validate(mobileAppBundle, b -> tenantId); + try { + MobileAppBundle savedMobileApp = mobileAppBundleDao.save(tenantId, mobileAppBundle); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(savedMobileApp).build()); + return savedMobileApp; + } catch (Exception e) { + checkConstraintViolation(e, + Map.of("android_app_id_unq_key", "Android mobile app already exists in another bundle!", + "ios_app_id_unq_key", "IOS mobile app already exists in another bundle!")); + throw e; + } + } + + @Override + public void updateOauth2Clients(TenantId tenantId, MobileAppBundleId mobileAppBundleId, List 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(tenantId, client); + } + for (MobileAppBundleOauth2Client client : newClientList) { + mobileAppBundleDao.addOauth2Client(tenantId, client); + } + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId) + .entityId(mobileAppBundleId).created(false).build()); + } + + @Override + public MobileAppBundle findMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId) { + log.trace("Executing findMobileAppBundleById [{}] [{}]", tenantId, mobileAppBundleId); + return mobileAppBundleDao.findById(tenantId, mobileAppBundleId.getId()); + } + + @Override + public PageData findMobileAppBundleInfosByTenantId(TenantId tenantId, PageLink pageLink) { + 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 findMobileAppBundleInfoById [{}] [{}]", tenantId, mobileAppIdBundle); + MobileAppBundleInfo mobileAppBundleInfo = mobileAppBundleDao.findInfoById(tenantId, mobileAppIdBundle); + if (mobileAppBundleInfo != null) { + fetchOauth2Clients(mobileAppBundleInfo); + } + return mobileAppBundleInfo; + } + + @Override + public MobileAppBundle findMobileAppBundleByPkgNameAndPlatform(TenantId tenantId, String pkgName, PlatformType platform) { + log.trace("Executing findMobileAppBundleByPkgNameAndPlatform, tenantId [{}], pkgName [{}], platform [{}]", tenantId, pkgName, platform); + checkNotNull(platform, PLATFORM_TYPE_IS_REQUIRED); + return mobileAppBundleDao.findByPkgNameAndPlatform(tenantId, pkgName, platform); + } + + @Override + public void deleteMobileAppBundleById(TenantId tenantId, MobileAppBundleId mobileAppBundleId) { + log.trace("Executing deleteMobileAppBundleById [{}]", mobileAppBundleId.getId()); + mobileAppBundleDao.removeById(tenantId, mobileAppBundleId.getId()); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(mobileAppBundleId).build()); + } + + @Override + public void deleteByTenantId(TenantId tenantId) { + log.trace("Executing deleteMobileAppsByTenantId, tenantId [{}]", tenantId); + mobileAppBundleDao.deleteByTenantId(tenantId); + } + + @Override + public Optional> findEntity(TenantId tenantId, EntityId entityId) { + return Optional.ofNullable(findMobileAppBundleById(tenantId, new MobileAppBundleId(entityId.getId()))); + } + + @Override + @Transactional + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + deleteMobileAppBundleById(tenantId, (MobileAppBundleId) id); + } + + @Override + public EntityType getEntityType() { + return EntityType.MOBILE_APP_BUNDLE; + } + + private void fetchOauth2Clients(MobileAppBundleInfo mobileAppBundleInfo) { + List 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/MobileAppDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java index 8da92e1eb3..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 @@ -15,26 +15,21 @@ */ package org.thingsboard.server.dao.mobile; -import org.thingsboard.server.common.data.id.MobileAppId; -import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.MobileApp; -import org.thingsboard.server.common.data.mobile.MobileAppOauth2Client; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; -import java.util.List; - public interface MobileAppDao extends Dao { - 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, PlatformType platformType, 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 66431a38dc..efd152f944 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 @@ -22,46 +22,45 @@ import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.MobileAppId; -import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.MobileApp; -import org.thingsboard.server.common.data.mobile.MobileAppInfo; -import org.thingsboard.server.common.data.mobile.MobileAppOauth2Client; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.oauth2.OAuth2ClientDao; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.service.Validator; -import java.util.Comparator; -import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; @Slf4j @Service public class MobileAppServiceImpl extends AbstractEntityService implements MobileAppService { - @Autowired - private OAuth2ClientDao oauth2ClientDao; + private static final String PLATFORM_TYPE_IS_REQUIRED = "Platform type is required if package name is specified"; + private static final String MOBILE_APP_BUNDLE_CONSTRAINT = "The mobile app referenced by the mobile bundle cannot be deleted!"; + @Autowired private MobileAppDao mobileAppDao; + @Autowired + private DataValidator mobileAppDataValidator; @Override public MobileApp saveMobileApp(TenantId tenantId, MobileApp mobileApp) { log.trace("Executing saveMobileApp [{}]", mobileApp); + mobileAppDataValidator.validate(mobileApp, a -> tenantId); try { MobileApp savedMobileApp = mobileAppDao.save(tenantId, mobileApp); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(savedMobileApp).build()); return savedMobileApp; } catch (Exception e) { checkConstraintViolation(e, - Map.of("mobile_app_unq_key", "Mobile app with such package already exists!")); + Map.of("mobile_app_pkg_name_platform_unq_key", "Mobile app with such package name and platform already exists!")); throw e; } } @@ -69,8 +68,15 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil @Override public void deleteMobileAppById(TenantId tenantId, MobileAppId mobileAppId) { log.trace("Executing deleteMobileAppById [{}]", mobileAppId.getId()); - mobileAppDao.removeById(tenantId, mobileAppId.getId()); - eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(mobileAppId).build()); + try { + mobileAppDao.removeById(tenantId, mobileAppId.getId()); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(mobileAppId).build()); + } catch (Exception e) { + checkConstraintViolation(e, + Map.of("fk_android_app_id", MOBILE_APP_BUNDLE_CONSTRAINT, + "fk_ios_app_id", MOBILE_APP_BUNDLE_CONSTRAINT)); + throw e; + } } @Override @@ -80,43 +86,9 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil } @Override - public PageData findMobileAppInfosByTenantId(TenantId tenantId, PageLink pageLink) { + public PageData findMobileAppsByTenantId(TenantId tenantId, PlatformType platformType, 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, platformType, pageLink); } @Override @@ -131,22 +103,22 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil } @Override - public void deleteMobileAppsByTenantId(TenantId tenantId) { - log.trace("Executing deleteMobileAppsByTenantId, tenantId [{}]", tenantId); - mobileAppDao.deleteByTenantId(tenantId); + public MobileApp findByBundleIdAndPlatformType(TenantId tenantId, MobileAppBundleId mobileAppBundleId, PlatformType platformType) { + log.trace("Executing findAndroidQrConfig, tenantId [{}], mobileAppBundleId [{}]", tenantId, mobileAppBundleId); + return mobileAppDao.findByBundleIdAndPlatformType(tenantId, mobileAppBundleId, platformType); } @Override - public void deleteByTenantId(TenantId tenantId) { - deleteMobileAppsByTenantId(tenantId); + public MobileApp findMobileAppByPkgNameAndPlatformType(String pkgName, PlatformType platformType) { + log.trace("Executing findMobileAppByPkgNameAndPlatformType, pkgName [{}], platform [{}]", pkgName, platformType); + Validator.checkNotNull(platformType, PLATFORM_TYPE_IS_REQUIRED); + return mobileAppDao.findByPkgNameAndPlatformType(TenantId.SYS_TENANT_ID, pkgName, platformType); } - private MobileAppInfo getMobileAppInfo(MobileApp mobileApp) { - List clients = oauth2ClientDao.findByMobileAppId(mobileApp.getUuidId()).stream() - .map(OAuth2ClientInfo::new) - .sorted(Comparator.comparing(OAuth2ClientInfo::getTitle)) - .collect(Collectors.toList()); - return new MobileAppInfo(mobileApp, clients); + @Override + public void deleteByTenantId(TenantId tenantId) { + log.trace("Executing deleteByTenantId, tenantId [{}]", tenantId); + mobileAppDao.deleteByTenantId(tenantId); } @Override 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 61% 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..e06f54a2e8 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,17 @@ package org.thingsboard.server.dao.mobile; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; +import org.thingsboard.server.common.data.oauth2.PlatformType; -public interface MobileAppSettingsService { +public interface QrCodeSettingService { - MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings settings); + QrCodeSettings saveQrCodeSettings(TenantId tenantId, QrCodeSettings qrCodeSettings); - MobileAppSettings getMobileAppSettings(TenantId tenantId); + QrCodeSettings findQrCodeSettings(TenantId tenantId); + + MobileApp findAppFromQrCodeSettings(TenantId sysTenantId, PlatformType platformType); void deleteByTenantId(TenantId tenantId); 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..ee3e59c60b --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/QrCodeSettingServiceImpl.java @@ -0,0 +1,134 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.mobile; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.BadgePosition; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QRCodeConfig; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; +import org.thingsboard.server.dao.service.DataValidator; + +import java.util.Map; + +import static org.thingsboard.server.common.data.oauth2.PlatformType.ANDROID; +import static org.thingsboard.server.common.data.oauth2.PlatformType.IOS; +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Service +@Slf4j +@RequiredArgsConstructor +public class QrCodeSettingServiceImpl extends AbstractCachedEntityService 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( + "qr_code_settings_tenant_id_unq_key", "Mobile application for specified tenant already exists!" + )); + throw e; + } + } + + @Override + public QrCodeSettings findQrCodeSettings(TenantId tenantId) { + log.trace("Executing getMobileAppSettings for tenant [{}] ", tenantId); + QrCodeSettings qrCodeSettings = cache.getAndPutInTransaction(tenantId, + () -> qrCodeSettingsDao.findByTenantId(tenantId), true); + return constructMobileAppSettings(qrCodeSettings); + } + + @Override + public MobileApp findAppFromQrCodeSettings(TenantId tenantId, PlatformType platformType) { + log.trace("Executing findAppQrCodeConfig for tenant [{}] ", tenantId); + QrCodeSettings qrCodeSettings = findQrCodeSettings(tenantId); + return qrCodeSettings.getMobileAppBundleId() != null ? mobileAppService.findByBundleIdAndPlatformType(tenantId, qrCodeSettings.getMobileAppBundleId(), platformType) : null; + } + + @Override + public void deleteByTenantId(TenantId tenantId) { + log.trace("Executing deleteByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + qrCodeSettingsDao.removeByTenantId(tenantId); + } + + @TransactionalEventListener(classes = QrCodeSettingsEvictEvent.class) + @Override + public void handleEvictEvent(QrCodeSettingsEvictEvent event) { + cache.evict(event.getTenantId()); + } + + private QrCodeSettings constructMobileAppSettings(QrCodeSettings qrCodeSettings) { + if (qrCodeSettings == null) { + qrCodeSettings = new QrCodeSettings(); + qrCodeSettings.setUseDefaultApp(true); + qrCodeSettings.setAndroidEnabled(true); + qrCodeSettings.setIosEnabled(true); + + QRCodeConfig qrCodeConfig = QRCodeConfig.builder() + .showOnHomePage(true) + .qrCodeLabelEnabled(true) + .qrCodeLabel(DEFAULT_QR_CODE_LABEL) + .badgeEnabled(true) + .badgePosition(BadgePosition.RIGHT) + .badgeEnabled(true) + .build(); + + qrCodeSettings.setQrCodeConfig(qrCodeConfig); + qrCodeSettings.setMobileAppBundleId(qrCodeSettings.getMobileAppBundleId()); + } + if (qrCodeSettings.isUseDefaultApp()) { + qrCodeSettings.setGooglePlayLink(googlePlayLink); + qrCodeSettings.setAppStoreLink(appStoreLink); + } else { + MobileApp androidApp = mobileAppService.findByBundleIdAndPlatformType(qrCodeSettings.getTenantId(), qrCodeSettings.getMobileAppBundleId(), ANDROID); + MobileApp iosApp = mobileAppService.findByBundleIdAndPlatformType(qrCodeSettings.getTenantId(), qrCodeSettings.getMobileAppBundleId(), IOS); + if (androidApp != null && androidApp.getStoreInfo() != null) { + qrCodeSettings.setGooglePlayLink(androidApp.getStoreInfo().getStoreLink()); + } + if (iosApp != null && iosApp.getStoreInfo() != null) { + qrCodeSettings.setAppStoreLink(iosApp.getStoreInfo().getStoreLink()); + } + } + return qrCodeSettings; + } + +} 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..adc66cb988 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.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..5e321bf69c 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.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..f155de6650 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.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 f85cd4bac4..28b6e6ebc2 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 @@ -449,11 +449,25 @@ public class ModelConstants { public static final String MOBILE_APP_TABLE_NAME = "mobile_app"; public static final String MOBILE_APP_PKG_NAME_PROPERTY = "pkg_name"; public static final String MOBILE_APP_APP_SECRET_PROPERTY = "app_secret"; - public static final String MOBILE_APP_OAUTH2_ENABLED_PROPERTY = "oauth2_enabled"; + public static final String MOBILE_APP_PLATFORM_TYPE_PROPERTY = "platform_type"; + public static final String MOBILE_APP_STATUS_PROPERTY = "status"; + public static final String MOBILE_APP_VERSION_INFO_PROPERTY = "version_info"; + public static final String MOBILE_APP_STORE_INFO_PROPERTY = "store_info"; - public static final String MOBILE_APP_OAUTH2_CLIENT_TABLE_NAME = "mobile_app_oauth2_client"; - public static final String MOBILE_APP_OAUTH2_CLIENT_CLIENT_ID_PROPERTY = "oauth2_client_id"; - public static final String MOBILE_APP_OAUTH2_CLIENT_MOBILE_APP_ID_PROPERTY = "mobile_app_id"; + /** + * Mobile application bundle constants. + */ + public static final String MOBILE_APP_BUNDLE_TABLE_NAME = "mobile_app_bundle"; + public static final String MOBILE_APP_BUNDLE_TITLE_PROPERTY = "title"; + public static final String MOBILE_APP_BUNDLE_DESCRIPTION_PROPERTY = "description"; + public static final String MOBILE_APP_BUNDLE_ANDROID_APP_ID_PROPERTY = "android_app_id"; + public static final String MOBILE_APP_BUNDLE_IOS_APP_ID_PROPERTY = "ios_app_id"; + public static final String MOBILE_APP_BUNDLE_LAYOUT_CONFIG_PROPERTY = "layout_config"; + public static final String MOBILE_APP_BUNDLE_OAUTH2_ENABLED_PROPERTY = "oauth2_enabled"; + + public static final String MOBILE_APP_BUNDLE_OAUTH2_CLIENT_TABLE_NAME = "mobile_app_bundle_oauth2_client"; + public static final String MOBILE_APP_BUNDLE_OAUTH2_CLIENT_CLIENT_ID_PROPERTY = "oauth2_client_id"; + public static final String MOBILE_APP_BUNDLE_OAUTH2_CLIENT_MOBILE_APP_BUNDLE_ID_PROPERTY = "mobile_app_bundle_id"; /** @@ -685,11 +699,12 @@ public class ModelConstants { /** * Mobile application settings constants. */ - public static final String MOBILE_APP_SETTINGS_TABLE_NAME = "mobile_app_settings"; - public static final String MOBILE_APP_SETTINGS_USE_DEFAULT_APP_PROPERTY = "use_default_app"; - public static final String MOBILE_APP_SETTINGS_ANDROID_CONFIG_PROPERTY = "android_config"; - public static final String MOBILE_APP_SETTINGS_IOS_CONFIG_PROPERTY = "ios_config"; - public static final String MOBILE_APP_SETTINGS_QR_CODE_CONFIG_PROPERTY = "qr_code_config"; + public static final String QR_CODE_SETTINGS_TABLE_NAME = "qr_code_settings"; + public static final String QR_CODE_SETTINGS_USE_DEFAULT_APP_PROPERTY = "use_default_app"; + public static final String QR_CODE_SETTINGS_ANDROID_ENABLED_PROPERTY = "android_enabled"; + public static final String QR_CODE_SETTINGS_IOS_ENABLED_PROPERTY = "ios_enabled"; + public static final String QR_CODE_SETTINGS_BUNDLE_ID_PROPERTY = "mobile_app_bundle_id"; + public static final String QR_CODE_SETTINGS_CONFIG_PROPERTY = "qr_code_config"; protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; 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 new file mode 100644 index 0000000000..47da9bba8f --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractMobileAppBundleEntity.java @@ -0,0 +1,114 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.MappedSuperclass; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.id.MobileAppBundleId; +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonConverter; + +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ID_COLUMN; + +@Data +@EqualsAndHashCode(callSuper = true) +@MappedSuperclass +public abstract class AbstractMobileAppBundleEntity 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/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppInfo.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java similarity index 51% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppInfo.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java index 4d85028e36..be7f7fe842 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppInfo.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleEntity.java @@ -13,35 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; +package org.thingsboard.server.dao.model.sql; -import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.id.MobileAppId; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; -import java.util.List; +import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_TABLE_NAME; -@EqualsAndHashCode(callSuper = true) @Data -@Schema -public class MobileAppInfo extends MobileApp { - - @Schema(description = "List of available oauth2 clients") - private List oauth2ClientInfos; - - public MobileAppInfo(MobileApp mobileApp, List oauth2ClientInfos) { - super(mobileApp); - this.oauth2ClientInfos = oauth2ClientInfos; - } +@EqualsAndHashCode(callSuper = true) +@Entity +@Table(name = MOBILE_APP_BUNDLE_TABLE_NAME) +public final class MobileAppBundleEntity extends AbstractMobileAppBundleEntity { - public MobileAppInfo() { + public MobileAppBundleEntity() { super(); } - public MobileAppInfo(MobileAppId mobileAppId) { - super(mobileAppId); + public MobileAppBundleEntity(MobileAppBundle mobileAppBundle) { + super(mobileAppBundle); } + @Override + public MobileAppBundle toData() { + 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..dc90f32e38 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleInfoEntity.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; + +@Data +@EqualsAndHashCode(callSuper = true) +public class MobileAppBundleInfoEntity extends AbstractMobileAppBundleEntity { + + private String androidPkgName; + private String iosPkgName; + private boolean qrCodeEnabled; + + public MobileAppBundleInfoEntity() { + super(); + } + + public MobileAppBundleInfoEntity(MobileAppBundleEntity mobileAppBundleEntity, String androidPkgName, String iosPkgName, boolean qrCodeEnabled) { + super(mobileAppBundleEntity); + this.androidPkgName = androidPkgName; + this.iosPkgName = iosPkgName; + this.qrCodeEnabled = qrCodeEnabled; + } + + @Override + public MobileAppBundleInfo toData() { + return new MobileAppBundleInfo(super.toMobileAppBundle(), androidPkgName, iosPkgName, qrCodeEnabled); + } +} 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 53% 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..152c8e3ebf 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.bundle.MobileAppBundleOauth2Client; import org.thingsboard.server.dao.model.ToData; import java.util.UUID; -import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_OAUTH2_CLIENT_MOBILE_APP_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_OAUTH2_CLIENT_CLIENT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_OAUTH2_CLIENT_TABLE_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_OAUTH2_CLIENT_CLIENT_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_OAUTH2_CLIENT_MOBILE_APP_BUNDLE_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_OAUTH2_CLIENT_TABLE_NAME; + @Data @Entity -@Table(name = MOBILE_APP_OAUTH2_CLIENT_TABLE_NAME) +@Table(name = MOBILE_APP_BUNDLE_OAUTH2_CLIENT_TABLE_NAME) @IdClass(MobileAppOauth2ClientCompositeKey.class) -public final class MobileAppOauth2ClientEntity implements ToData { +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(); - oauth2ClientId = domainOauth2Provider.getOAuth2ClientId().getId(); + public MobileAppBundleOauth2ClientEntity(MobileAppBundleOauth2Client mobileAppBundleOauth2Client) { + mobileAppBundleId = mobileAppBundleOauth2Client.getMobileAppBundleId().getId(); + oauth2ClientId = mobileAppBundleOauth2Client.getOAuth2ClientId().getId(); } @Override - public MobileAppOauth2Client toData() { - MobileAppOauth2Client result = new MobileAppOauth2Client(); - result.setMobileAppId(new MobileAppId(mobileAppId)); + public MobileAppBundleOauth2Client toData() { + MobileAppBundleOauth2Client result = new MobileAppBundleOauth2Client(); + result.setMobileAppBundleId(new MobileAppBundleId(mobileAppBundleId)); result.setOAuth2ClientId(new OAuth2ClientId(oauth2ClientId)); return result; } 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..45c4ba8cbd 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,25 @@ */ package org.thingsboard.server.dao.model.sql; +import com.fasterxml.jackson.databind.JsonNode; import jakarta.persistence.Column; +import jakarta.persistence.Convert; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.MobileAppId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppStatus; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppVersionInfo; +import org.thingsboard.server.common.data.mobile.app.StoreInfo; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonConverter; import java.util.UUID; @@ -45,8 +54,21 @@ public class MobileAppEntity extends BaseSqlEntity { @Column(name = ModelConstants.MOBILE_APP_APP_SECRET_PROPERTY) private String appSecret; - @Column(name = ModelConstants.MOBILE_APP_OAUTH2_ENABLED_PROPERTY) - private Boolean oauth2Enabled; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.MOBILE_APP_PLATFORM_TYPE_PROPERTY) + private PlatformType platformType; + + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.MOBILE_APP_STATUS_PROPERTY) + private MobileAppStatus status; + + @Convert(converter = JsonConverter.class) + @Column(name = ModelConstants.MOBILE_APP_VERSION_INFO_PROPERTY) + private JsonNode versionInfo; + + @Convert(converter = JsonConverter.class) + @Column(name = ModelConstants.MOBILE_APP_STORE_INFO_PROPERTY) + private JsonNode storeInfo; public MobileAppEntity() { super(); @@ -59,7 +81,10 @@ public class MobileAppEntity extends BaseSqlEntity { } this.pkgName = mobile.getPkgName(); this.appSecret = mobile.getAppSecret(); - this.oauth2Enabled = mobile.isOauth2Enabled(); + this.platformType = mobile.getPlatformType(); + this.status = mobile.getStatus(); + this.versionInfo = toJson(mobile.getVersionInfo()); + this.storeInfo = toJson(mobile.getStoreInfo()); } @Override @@ -72,7 +97,10 @@ public class MobileAppEntity extends BaseSqlEntity { mobile.setCreatedTime(createdTime); mobile.setPkgName(pkgName); mobile.setAppSecret(appSecret); - mobile.setOauth2Enabled(oauth2Enabled); + mobile.setPlatformType(platformType); + mobile.setStatus(status); + mobile.setVersionInfo(fromJson(versionInfo, MobileAppVersionInfo.class)); + mobile.setStoreInfo(fromJson(storeInfo, StoreInfo.class)); return mobile; } } 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..81c187a53b --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QrCodeSettingsEntity.java @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.MobileAppBundleId; +import org.thingsboard.server.common.data.id.QrCodeSettingsId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QRCodeConfig; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonConverter; + +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@Entity +@Table(name = ModelConstants.QR_CODE_SETTINGS_TABLE_NAME) +public class QrCodeSettingsEntity extends BaseSqlEntity { + + @Column(name = ModelConstants.TENANT_ID_COLUMN, columnDefinition = "uuid") + protected UUID tenantId; + + @Column(name = ModelConstants.QR_CODE_SETTINGS_USE_DEFAULT_APP_PROPERTY) + private boolean useDefaultApp; + + @Column(name = ModelConstants.QR_CODE_SETTINGS_ANDROID_ENABLED_PROPERTY) + private boolean androidEnabled; + + @Column(name = ModelConstants.QR_CODE_SETTINGS_IOS_ENABLED_PROPERTY) + private boolean iosEnabled; + + @Column(name = ModelConstants.QR_CODE_SETTINGS_BUNDLE_ID_PROPERTY) + private UUID mobileAppBundleId; + + @Convert(converter = JsonConverter.class) + @Column(name = ModelConstants.QR_CODE_SETTINGS_CONFIG_PROPERTY) + private JsonNode qrCodeConfig; + + public QrCodeSettingsEntity(QrCodeSettings qrCodeSettings) { + this.setId(qrCodeSettings.getUuidId()); + this.setCreatedTime(qrCodeSettings.getCreatedTime()); + this.tenantId = qrCodeSettings.getTenantId().getId(); + this.useDefaultApp = qrCodeSettings.isUseDefaultApp(); + this.androidEnabled = qrCodeSettings.isAndroidEnabled(); + this.iosEnabled = qrCodeSettings.isIosEnabled(); + if (qrCodeSettings.getMobileAppBundleId() != null) { + this.mobileAppBundleId = qrCodeSettings.getMobileAppBundleId().getId(); + } + this.qrCodeConfig = toJson(qrCodeSettings.getQrCodeConfig()); + } + + @Override + public QrCodeSettings toData() { + QrCodeSettings qrCodeSettings = new QrCodeSettings(new QrCodeSettingsId(getUuid())); + qrCodeSettings.setCreatedTime(createdTime); + qrCodeSettings.setTenantId(TenantId.fromUUID(tenantId)); + qrCodeSettings.setUseDefaultApp(useDefaultApp); + qrCodeSettings.setAndroidEnabled(androidEnabled); + qrCodeSettings.setIosEnabled(iosEnabled); + if (mobileAppBundleId != null) { + qrCodeSettings.setMobileAppBundleId(new MobileAppBundleId(mobileAppBundleId)); + } + qrCodeSettings.setQrCodeConfig(fromJson(qrCodeConfig, QRCodeConfig.class)); + return qrCodeSettings; + } + +} 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/MobileAppBundleDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppBundleDataValidator.java new file mode 100644 index 0000000000..e2a2625f7d --- /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.app.MobileApp; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.mobile.MobileAppDao; +import org.thingsboard.server.dao.service.DataValidator; + +@Component +@AllArgsConstructor +public class MobileAppBundleDataValidator extends DataValidator { + + @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/service/validator/MobileAppDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppDataValidator.java new file mode 100644 index 0000000000..d91ea8251d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppDataValidator.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppStatus; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; + +@Component +@AllArgsConstructor +public class MobileAppDataValidator extends DataValidator { + + @Override + protected void validateDataImpl(TenantId tenantId, MobileApp mobileApp) { + if (mobileApp.getPlatformType() == PlatformType.ANDROID) { + if (mobileApp.getStoreInfo() != null && + (mobileApp.getStoreInfo().getSha256CertFingerprints() == null || mobileApp.getStoreInfo().getStoreLink() == null)) { + throw new DataValidationException("Sha256CertFingerprints and store link are required"); + } + } else if (mobileApp.getPlatformType() == PlatformType.IOS) { + if (mobileApp.getStoreInfo() != null && + (mobileApp.getStoreInfo().getAppId() == null || mobileApp.getStoreInfo().getStoreLink() == null)) { + throw new DataValidationException("AppId and store link are required"); + } + } else { + throw new DataValidationException("Wrong application platform type"); + } + if (mobileApp.getStatus() == MobileAppStatus.PUBLISHED && mobileApp.getStoreInfo() == null) { + throw new DataValidationException("Store info is required for published apps"); + } + } +} 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..991211d676 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QrCodeSettingsDataValidator.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import lombok.AllArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.MobileAppBundleId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppStatus; +import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.mobile.MobileAppDao; +import org.thingsboard.server.dao.service.DataValidator; + +@Component +@AllArgsConstructor +public class QrCodeSettingsDataValidator extends DataValidator { + + @Autowired + MobileAppDao mobileAppDao; + + @Override + protected void validateDataImpl(TenantId tenantId, QrCodeSettings qrCodeSettings) { + MobileAppBundleId mobileAppBundleId = qrCodeSettings.getMobileAppBundleId(); + if (!qrCodeSettings.isUseDefaultApp() && (mobileAppBundleId == null)) { + throw new DataValidationException("Mobile app bundle is required to use custom application!"); + } + if (!qrCodeSettings.isUseDefaultApp()) { + if (qrCodeSettings.isAndroidEnabled()) { + MobileApp androidApp = mobileAppDao.findByBundleIdAndPlatformType(tenantId, mobileAppBundleId, PlatformType.ANDROID); + if (androidApp != null && androidApp.getStatus() != MobileAppStatus.PUBLISHED) { + throw new DataValidationException("The mobile app bundle references an Android app that has not been published!"); + } + } + if (qrCodeSettings.isIosEnabled()) { + MobileApp iosApp = mobileAppDao.findByBundleIdAndPlatformType(tenantId, mobileAppBundleId, PlatformType.IOS); + if (iosApp != null && iosApp.getStatus() != MobileAppStatus.PUBLISHED) { + throw new DataValidationException("The mobile app bundle references an iOS app that has not been published!"); + } + } + } + } +} 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..7dcf9786fe --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppBundleDao.java @@ -0,0 +1,101 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.mobile; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.MobileAppBundleId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleOauth2Client; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.mobile.MobileAppBundleDao; +import org.thingsboard.server.dao.model.sql.MobileAppBundleEntity; +import org.thingsboard.server.dao.model.sql.MobileAppBundleOauth2ClientEntity; +import org.thingsboard.server.dao.model.sql.MobileAppOauth2ClientCompositeKey; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.List; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +@SqlDao +public class JpaMobileAppBundleDao extends JpaAbstractDao 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 findInfosByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(mobileAppBundleRepository.findInfoByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); + } + + @Override + public MobileAppBundleInfo findInfoById(TenantId tenantId, MobileAppBundleId mobileAppBundleId) { + return DaoUtil.getData(mobileAppBundleRepository.findInfoById(mobileAppBundleId.getId())); + } + + @Override + public List findOauth2ClientsByMobileAppBundleId(TenantId tenantId, MobileAppBundleId mobileAppBundleId) { + return DaoUtil.convertDataList(mobileOauth2ProviderRepository.findAllByMobileAppBundleId(mobileAppBundleId.getId())); + } + + @Override + public void addOauth2Client(TenantId tenantId, MobileAppBundleOauth2Client mobileAppBundleOauth2Client) { + mobileOauth2ProviderRepository.save(new MobileAppBundleOauth2ClientEntity(mobileAppBundleOauth2Client)); + } + + @Override + public void removeOauth2Client(TenantId tenantId, MobileAppBundleOauth2Client mobileAppBundleOauth2Client) { + mobileOauth2ProviderRepository.deleteById(new MobileAppOauth2ClientCompositeKey(mobileAppBundleOauth2Client.getMobileAppBundleId().getId(), + mobileAppBundleOauth2Client.getOAuth2ClientId().getId())); + } + + @Override + public MobileAppBundle findByPkgNameAndPlatform(TenantId tenantId, String pkgName, PlatformType platform) { + return DaoUtil.getData(mobileAppBundleRepository.findByPkgNameAndPlatformType(pkgName, platform)); + } + + @Override + public void deleteByTenantId(TenantId tenantId) { + mobileAppBundleRepository.deleteByTenantId(tenantId.getId()); + } + + @Override + public EntityType getEntityType() { + return EntityType.MOBILE_APP_BUNDLE; + } + +} + 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..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 @@ -19,21 +19,18 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.MobileApp; -import org.thingsboard.server.common.data.mobile.MobileAppOauth2Client; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.mobile.MobileAppDao; import org.thingsboard.server.dao.model.sql.MobileAppEntity; -import org.thingsboard.server.dao.model.sql.MobileAppOauth2ClientCompositeKey; -import org.thingsboard.server.dao.model.sql.MobileAppOauth2ClientEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; -import java.util.List; import java.util.UUID; @Component @@ -42,7 +39,6 @@ import java.util.UUID; public class JpaMobileAppDao extends JpaAbstractDao implements MobileAppDao { private final MobileAppRepository mobileAppRepository; - private final MobileAppOauth2ClientRepository mobileOauth2ProviderRepository; @Override protected Class getEntityClass() { @@ -55,29 +51,27 @@ 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 MobileApp findByBundleIdAndPlatformType(TenantId tenantId, MobileAppBundleId mobileAppBundleId, PlatformType platformType) { + return switch (platformType) { + case ANDROID -> DaoUtil.getData(mobileAppRepository.findAndroidAppByBundleId(mobileAppBundleId.getId())); + case IOS -> DaoUtil.getData(mobileAppRepository.findIOSAppByBundleId(mobileAppBundleId.getId())); + default -> null; + }; } @Override - public List findOauth2ClientsByMobileAppId(TenantId tenantId, MobileAppId mobileAppId) { - return DaoUtil.convertDataList(mobileOauth2ProviderRepository.findAllByMobileAppId(mobileAppId.getId())); + public PageData findByTenantId(TenantId tenantId, PlatformType platformType, PageLink pageLink) { + return DaoUtil.toPageData(mobileAppRepository.findByTenantId(tenantId.getId(), platformType, pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } @Override - public void addOauth2Client(MobileAppOauth2Client mobileAppOauth2Client) { - mobileOauth2ProviderRepository.save(new MobileAppOauth2ClientEntity(mobileAppOauth2Client)); - } - - @Override - public void removeOauth2Client(MobileAppOauth2Client mobileAppOauth2Client) { - mobileOauth2ProviderRepository.deleteById(new MobileAppOauth2ClientCompositeKey(mobileAppOauth2Client.getMobileAppId().getId(), - mobileAppOauth2Client.getOAuth2ClientId().getId())); + public void deleteByTenantId(TenantId tenantId) { + mobileAppRepository.deleteByTenantId(tenantId.getId()); } @Override - public void deleteByTenantId(TenantId tenantId) { - mobileAppRepository.deleteByTenantId(tenantId.getId()); + public MobileApp findByPkgNameAndPlatformType(TenantId tenantId, String pkgName, PlatformType platform) { + return DaoUtil.getData(mobileAppRepository.findByPkgNameAndPlatformType(pkgName, platform)); } @Override 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..3a478e745c 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.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..c3aeb6c32d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppBundleRepository.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.mobile; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.dao.model.sql.MobileAppBundleEntity; +import org.thingsboard.server.dao.model.sql.MobileAppBundleInfoEntity; + +import java.util.UUID; + +public interface MobileAppBundleRepository extends JpaRepository { + + @Query("SELECT new org.thingsboard.server.dao.model.sql.MobileAppBundleInfoEntity(b, andApp.pkgName, iosApp.pkgName, " + + "((andApp.status IS NOT NULL AND andApp.status = 'PUBLISHED') OR (iosApp.status IS NOT NULL AND iosApp.status = 'PUBLISHED'))) " + + "FROM MobileAppBundleEntity b " + + "LEFT JOIN MobileAppEntity andApp ON b.androidAppId = andApp.id " + + "LEFT JOIN MobileAppEntity iosApp ON b.iosAppID = iosApp.id " + + "WHERE b.tenantId = :tenantId AND " + + "(:searchText is NULL OR ilike(b.title, concat('%', :searchText, '%')) = true)") + Page findInfoByTenantId(@Param("tenantId") UUID tenantId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.MobileAppBundleInfoEntity(b, andApp.pkgName, iosApp.pkgName, " + + "((andApp.status IS NOT NULL AND andApp.status = 'PUBLISHED') OR (iosApp.status IS NOT NULL AND iosApp.status = 'PUBLISHED'))) " + + "FROM MobileAppBundleEntity b " + + "LEFT JOIN MobileAppEntity andApp on b.androidAppId = andApp.id " + + "LEFT JOIN MobileAppEntity iosApp on b.iosAppID = iosApp.id " + + "WHERE b.id = :bundleId ") + MobileAppBundleInfoEntity findInfoById(UUID bundleId); + + @Query("SELECT b " + + "FROM MobileAppBundleEntity b " + + "LEFT JOIN MobileAppEntity a ON b.androidAppId = a.id OR b.iosAppID = a.id " + + "WHERE a.pkgName = :pkgName AND a.platformType = :platformType") + MobileAppBundleEntity findByPkgNameAndPlatformType(@Param("pkgName") String pkgName, + @Param("platformType") PlatformType platformType); + + @Query("SELECT b FROM MobileAppBundleEntity b WHERE b.tenantId = :tenantId AND " + + "(:searchText is NULL OR ilike(b.title, concat('%', :searchText, '%')) = true)") + Page 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..dcf67016ce 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 @@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.dao.model.sql.MobileAppEntity; import java.util.UUID; @@ -29,14 +30,24 @@ import java.util.UUID; public interface MobileAppRepository extends JpaRepository { @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); + MobileAppEntity findByPkgNameAndPlatformType(@Param("pkgName") String pkgName, @Param("platformType") PlatformType platformType); + @Transactional @Modifying @Query("DELETE FROM MobileAppEntity r WHERE r.tenantId = :tenantId") void deleteByTenantId(@Param("tenantId") UUID tenantId); + @Query("SELECT a FROM MobileAppEntity a LEFT JOIN MobileAppBundleEntity b ON b.androidAppId = a.id WHERE b.id = :bundleId") + MobileAppEntity findAndroidAppByBundleId(@Param("bundleId") UUID bundleId); + + @Query("SELECT a FROM MobileAppEntity a LEFT JOIN MobileAppBundleEntity b ON b.iosAppID = a.id WHERE b.id = :bundleId") + MobileAppEntity findIOSAppByBundleId(@Param("bundleId") UUID bundleId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/QrCodeSettingsRepository.java similarity index 76% rename from dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/mobile/QrCodeSettingsRepository.java index bb6e62f3fe..c8ed28aa28 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/QrCodeSettingsRepository.java @@ -20,17 +20,17 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; -import org.thingsboard.server.dao.model.sql.MobileAppSettingsEntity; +import org.thingsboard.server.dao.model.sql.QrCodeSettingsEntity; import java.util.UUID; -public interface MobileAppSettingsRepository extends JpaRepository { +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..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,33 @@ 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 MobileAppOauth2ClientEntity mc on mc.oauth2ClientId = c.id " + - "WHERE mc.mobileAppId = :mobileAppId ") - List 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..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 @@ -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 { - UserMobileInfo newMobileInfo = new UserMobileInfo(); + UserMobileSessionInfo mobileInfo = findMobileSessionInfo(tenantId, userId).orElseGet(() -> { + UserMobileSessionInfo newMobileInfo = new UserMobileSessionInfo(); newMobileInfo.setSessions(new HashMap<>()); return newMobileInfo; }); @@ -459,12 +459,12 @@ public class UserServiceImpl extends AbstractCachedEntityService findMobileSessions(TenantId tenantId, UserId userId) { - return findMobileInfo(tenantId, userId).map(UserMobileInfo::getSessions).orElse(Collections.emptyMap()); + return findMobileSessionInfo(tenantId, userId).map(UserMobileSessionInfo::getSessions).orElse(Collections.emptyMap()); } @Override public MobileSessionInfo findMobileSession(TenantId tenantId, UserId userId, String mobileToken) { - return findMobileInfo(tenantId, userId).map(mobileInfo -> mobileInfo.getSessions().get(mobileToken)).orElse(null); + return findMobileSessionInfo(tenantId, userId).map(mobileInfo -> mobileInfo.getSessions().get(mobileToken)).orElse(null); } @Override @@ -475,9 +475,9 @@ public class UserServiceImpl extends AbstractCachedEntityService 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, UserMobileInfo.class)); + .map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileSessionInfo.class)); } @Override diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 6b4f1a6c09..04627e7b0c 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -127,3 +127,5 @@ CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); CREATE INDEX IF NOT EXISTS idx_resource_type_public_resource_key ON resource(resource_type, public_resource_key); + +CREATE INDEX IF NOT EXISTS mobile_app_bundle_tenant_id ON mobile_app_bundle(tenant_id); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index a2f6e461bf..edcb8645dd 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -622,9 +622,27 @@ CREATE TABLE IF NOT EXISTS mobile_app ( id uuid NOT NULL CONSTRAINT mobile_app_pkey PRIMARY KEY, created_time bigint NOT NULL, tenant_id uuid, - pkg_name varchar(255) UNIQUE, + pkg_name varchar(255), app_secret varchar(2048), - oauth2_enabled boolean + platform_type varchar(32), + status varchar(32), + version_info varchar(100000), + store_info varchar(16384), + CONSTRAINT mobile_app_pkg_name_platform_unq_key UNIQUE (pkg_name, platform_type) +); + +CREATE TABLE IF NOT EXISTS mobile_app_bundle ( + id uuid NOT NULL CONSTRAINT mobile_app_bundle_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid, + title varchar(255), + description varchar(1024), + android_app_id uuid UNIQUE, + ios_app_id uuid UNIQUE, + layout_config varchar(16384), + oauth2_enabled boolean, + CONSTRAINT fk_android_app_id FOREIGN KEY (android_app_id) REFERENCES mobile_app(id), + CONSTRAINT fk_ios_app_id FOREIGN KEY (ios_app_id) REFERENCES mobile_app(id) ); CREATE TABLE IF NOT EXISTS domain_oauth2_client ( @@ -634,10 +652,10 @@ CREATE TABLE IF NOT EXISTS domain_oauth2_client ( CONSTRAINT fk_oauth2_client FOREIGN KEY (oauth2_client_id) REFERENCES oauth2_client(id) ON DELETE CASCADE ); -CREATE TABLE IF NOT EXISTS mobile_app_oauth2_client ( - mobile_app_id uuid NOT NULL, +CREATE TABLE IF NOT EXISTS mobile_app_bundle_oauth2_client ( + mobile_app_bundle_id uuid NOT NULL, oauth2_client_id uuid NOT NULL, - CONSTRAINT fk_domain FOREIGN KEY (mobile_app_id) REFERENCES mobile_app(id) ON DELETE CASCADE, + CONSTRAINT fk_domain FOREIGN KEY (mobile_app_bundle_id) REFERENCES mobile_app_bundle(id) ON DELETE CASCADE, CONSTRAINT fk_oauth2_client FOREIGN KEY (oauth2_client_id) REFERENCES oauth2_client(id) ON DELETE CASCADE ); @@ -876,13 +894,14 @@ CREATE TABLE IF NOT EXISTS queue_stats ( CONSTRAINT queue_stats_name_unq_key UNIQUE (tenant_id, queue_name, service_id) ); -CREATE TABLE IF NOT EXISTS mobile_app_settings ( +CREATE TABLE IF NOT EXISTS qr_code_settings ( id uuid NOT NULL CONSTRAINT mobile_app_settings_pkey PRIMARY KEY, created_time bigint NOT NULL, tenant_id uuid NOT NULL, use_default_app boolean, - android_config VARCHAR(1000), - ios_config VARCHAR(1000), + android_enabled boolean, + ios_enabled boolean, + mobile_app_bundle_id uuid, qr_code_config VARCHAR(100000), - CONSTRAINT mobile_app_settings_tenant_id_unq_key UNIQUE (tenant_id) + CONSTRAINT qr_code_settings_tenant_id_unq_key UNIQUE (tenant_id) ); 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 ec093d0947..4ab9bb6d86 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 @@ -20,22 +20,18 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.mobile.MobileApp; -import org.thingsboard.server.common.data.mobile.MobileAppInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2Client; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileAppStatus; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -import static org.thingsboard.server.dao.oauth2.OAuth2Utils.OAUTH2_AUTHORIZATION_PATH_TEMPLATE; @DaoSqlTest public class MobileAppServiceTest extends AbstractServiceTest { @@ -54,7 +50,7 @@ public class MobileAppServiceTest extends AbstractServiceTest { @Test public void testSaveMobileApp() { - MobileApp MobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "mobileApp.ce", true); + MobileApp MobileApp = validMobileApp(SYSTEM_TENANT_ID, "mobileApp.ce", PlatformType.IOS); MobileApp savedMobileApp = mobileAppService.saveMobileApp(SYSTEM_TENANT_ID, MobileApp); MobileApp retrievedMobileApp = mobileAppService.findMobileAppById(savedMobileApp.getTenantId(), savedMobileApp.getId()); @@ -74,43 +70,23 @@ public class MobileAppServiceTest extends AbstractServiceTest { @Test public void testGetTenantMobileApps() { - List 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 oAuth2Client = validMobileApp(SYSTEM_TENANT_ID, StringUtils.randomAlphabetic(5), PlatformType.ANDROID); MobileApp savedOauth2Client = mobileAppService.saveMobileApp(SYSTEM_TENANT_ID, oAuth2Client); - MobileApps.add(savedOauth2Client); + mobileApps.add(savedOauth2Client); } - PageData 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, null, new PageLink(10, 0)); + assertThat(retrieved.getData()).containsOnlyOnceElementsOf(mobileApps); } - @Test - public void tesGetMobileAppInfo() { - OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "Test google client"); - OAuth2Client savedOauth2Client = oAuth2ClientService.saveOAuth2Client(SYSTEM_TENANT_ID, oAuth2Client); - List 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) { + private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, PlatformType platformType) { MobileApp MobileApp = new MobileApp(); MobileApp.setTenantId(tenantId); MobileApp.setPkgName(mobileAppName); + MobileApp.setStatus(MobileAppStatus.DRAFT); MobileApp.setAppSecret(StringUtils.randomAlphanumeric(24)); - MobileApp.setOauth2Enabled(oauth2Enabled); + MobileApp.setPlatformType(platformType); return MobileApp; } } 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..a4a8e86b4d 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; @@ -123,8 +124,9 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.mobile.MobileApp; -import org.thingsboard.server.common.data.mobile.MobileAppInfo; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; @@ -2107,12 +2109,12 @@ public class RestClient implements Closeable { }, params).getBody(); } - public List getTenantOAuth2Clients() { + public PageData getTenantOAuth2Clients() { return restTemplate.exchange( baseURL + "/api/oauth2/client/infos", HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }).getBody(); } @@ -2137,12 +2139,12 @@ public class RestClient implements Closeable { restTemplate.delete(baseURL + "/api/oauth2/client/{id}", oAuth2ClientId.getId()); } - public List getTenantDomainInfos() { + public PageData getTenantDomainInfos() { return restTemplate.exchange( baseURL + "/api/domain/infos", HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }).getBody(); } @@ -2171,19 +2173,19 @@ public class RestClient implements Closeable { restTemplate.postForLocation(baseURL + "/api/domain/{id}/oauth2Clients", oauth2ClientIds, domainId.getId()); } - public List getTenantMobileAppInfos() { + public PageData getTenantMobileApps() { return restTemplate.exchange( - baseURL + "/api/mobileApp/infos", + baseURL + "/api/mobile/app", HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + 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 +2196,45 @@ public class RestClient implements Closeable { } public MobileApp saveMobileApp(MobileApp mobileApp) { - return restTemplate.postForEntity(baseURL + "/api/mobileApp", mobileApp, MobileApp.class).getBody(); + return restTemplate.postForEntity(baseURL + "/api/mobile/app", mobileApp, MobileApp.class).getBody(); } public void deleteMobileApp(MobileAppId mobileAppId) { - restTemplate.delete(baseURL + "/api/mobileApp/{id}", mobileAppId.getId()); + restTemplate.delete(baseURL + "/api/mobile/app/{id}", mobileAppId.getId()); } - public void updateMobileAppOauth2Clients(MobileAppId mobileAppId, UUID[] oauth2ClientIds) { - restTemplate.postForLocation(baseURL + "/api/mobileApp/{id}/oauth2Clients", oauth2ClientIds, mobileAppId.getId()); + public PageData 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/bundle/{id}", MobileAppBundle.class, mobileAppBundleId.getId()); + return Optional.ofNullable(mobileApp.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public MobileAppBundle saveMobileBundle(MobileAppBundle mobileAppBundle) { + return restTemplate.postForEntity(baseURL + "/api/mobile/bundle", mobileAppBundle, MobileAppBundle.class).getBody(); + } + + public void deleteMobileBundle(MobileAppBundleId mobileAppBundleId) { + restTemplate.delete(baseURL + "/api/mobile/bundle/{id}", mobileAppBundleId.getId()); + } + + public void updateMobileAppBundleOauth2Clients(MobileAppBundleId mobileAppBundleId, UUID[] oauth2ClientIds) { + restTemplate.postForLocation(baseURL + "/api/mobile/bundle/{id}/oauth2Clients", oauth2ClientIds, mobileAppBundleId.getId()); } public String getLoginProcessingUrl() { diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 7dd4505f29..b517bb0051 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -61,6 +61,7 @@ import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.dao.mobile.MobileAppBundleService; import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.nosql.TbResultSetFuture; @@ -355,6 +356,8 @@ public interface TbContext { MobileAppService getMobileAppService(); + MobileAppBundleService getMobileAppBundleService(); + SlackService getSlackService(); boolean isExternalNodeForceAck(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index 1e1b031259..b1662dd5c2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.DomainId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.MobileAppId; import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRuleId; @@ -157,6 +158,9 @@ public class TenantIdLoader { case MOBILE_APP: tenantEntity = ctx.getMobileAppService().findMobileAppById(ctxTenantId, new MobileAppId(id)); break; + case MOBILE_APP_BUNDLE: + tenantEntity = ctx.getMobileAppBundleService().findMobileAppBundleById(ctxTenantId, new MobileAppBundleId(id)); + break; default: throw new RuntimeException("Unexpected entity type: " + entityId.getEntityType()); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 3e0e479c80..abba10b450 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -52,7 +52,8 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; -import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.app.MobileApp; +import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; @@ -72,6 +73,7 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.mobile.MobileAppBundleService; import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRuleService; @@ -151,6 +153,8 @@ public class TenantIdLoaderTest { private DomainService domainService; @Mock private MobileAppService mobileAppService; + @Mock + private MobileAppBundleService mobileAppBundleService; private TenantId tenantId; private TenantProfileId tenantProfileId; @@ -392,6 +396,12 @@ public class TenantIdLoaderTest { when(ctx.getMobileAppService()).thenReturn(mobileAppService); doReturn(mobileApp).when(mobileAppService).findMobileAppById(eq(tenantId), any()); break; + case MOBILE_APP_BUNDLE: + MobileAppBundle mobileAppBundle = new MobileAppBundle(); + mobileAppBundle.setTenantId(tenantId); + when(ctx.getMobileAppBundleService()).thenReturn(mobileAppBundleService); + doReturn(mobileAppBundle).when(mobileAppBundleService).findMobileAppBundleById(eq(tenantId), any()); + break; default: throw new RuntimeException("Unexpected originator EntityType " + entityType); } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 50978662df..ed95c17cfd 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,13 @@ 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; + case EntityType.MOBILE_APP_BUNDLE: + observable = this.mobileAppService.getMobileAppBundleInfoById(entityId, config); + break; } return observable; } @@ -462,6 +472,14 @@ 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; + case EntityType.MOBILE_APP_BUNDLE: + pageLink.sortOrder.property = 'title'; + entitiesObservable = this.mobileAppService.getTenantMobileAppBundleInfos(pageLink, 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 a506d2805e..37470768ca 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,10 @@ 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'; +import { PlatformType } from '@shared/models/oauth2.models'; @Injectable({ providedIn: 'root' @@ -32,25 +33,48 @@ 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, 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 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) { + 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(`/api/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/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/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 ed1c3130a7..cf08fe2a56 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -65,6 +65,10 @@ export enum MenuId { notification_recipients = 'notification_recipients', notification_templates = 'notification_templates', notification_rules = 'notification_rules', + mobile_center = 'mobile_center', + mobile_apps = 'mobile_apps', + mobile_bundles = 'mobile_bundles', + mobile_qr_code_widget = 'mobile_qr_code_widget', settings = 'settings', general = 'general', mail_server = 'mail_server', @@ -73,13 +77,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', @@ -272,6 +274,47 @@ 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_bundles, + { + id: MenuId.mobile_bundles, + name: 'mobile.bundles', + type: 'link', + path: '/mobile-center/bundles', + icon: 'mdi:package' + } + ], + [ + MenuId.mobile_qr_code_widget, + { + id: MenuId.mobile_qr_code_widget, + name: 'mobile.qr-code-widget', + fullName: 'mobile.qr-code-widget', + type: 'link', + path: '/mobile-center/qr-code-widget', + icon: 'qr_code' + } + ], [ MenuId.settings, { @@ -357,17 +400,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, { @@ -419,16 +451,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, { @@ -698,14 +720,21 @@ const defaultUserMenuMap = new Map([ {id: MenuId.notification_rules} ] }, + { + id: MenuId.mobile_center, + pages: [ + {id: MenuId.mobile_bundles}, + {id: MenuId.mobile_apps}, + {id: MenuId.mobile_qr_code_widget} + ] + }, { 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} ] }, { @@ -717,7 +746,6 @@ const defaultUserMenuMap = new Map([ id: MenuId.oauth2, pages: [ {id: MenuId.domains}, - {id: MenuId.mobile_apps}, {id: MenuId.clients} ] } @@ -788,6 +816,13 @@ const defaultUserMenuMap = new Map([ {id: MenuId.notification_rules} ] }, + { + id: MenuId.mobile_center, + pages: [ + {id: MenuId.mobile_bundles}, + {id: MenuId.mobile_apps} + ] + }, {id: MenuId.api_usage}, { id: MenuId.settings, @@ -801,7 +836,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} + ] + } ] } ] @@ -846,7 +887,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 6bebae3273..e6c85b70dd 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 @@ -160,6 +160,7 @@ *matHeaderCellDef [style]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable"> {{ column.ignoreTranslate ? column.title : (column.title | translate) }} - + @@ -182,6 +183,7 @@ ([ - [EntityType.DOMAIN, 'oauth2ClientInfos'], - [EntityType.MOBILE_APP, 'oauth2ClientInfos'] -]); +import { isEqual, 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)) { + if (entitiesList.length) { + this.entityDetailsPrefixUrl = baseDetailsPageByEntityType.get(entitiesList[0].id.entityType as EntityType); + } + } else { + entitiesList = []; + } + if (!isEqual(entitiesList, this.subEntities)) { + this.subEntities = entitiesList; } } } 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/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..7ae77ba8fa 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 848b727e61..2374b8b2ee 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 @@ -18,7 +18,7 @@ import { ChangeDetectorRef, Component, ElementRef, Input, NgZone, OnDestroy, OnI import { PageComponent } from '@shared/components/page.component'; import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; -import { BadgePosition, MobileAppSettings } from '@shared/models/mobile-app.models'; +import { BadgePosition, QrCodeSettings } from '@shared/models/mobile-app.models'; import { MobileApplicationService } from '@core/http/mobile-application.service'; import { WidgetContext } from '@home/models/widget-component.models'; import { UtilsService } from '@core/services/utils.service'; @@ -39,7 +39,7 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI private readonly destroy$ = new Subject(); 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; } @@ -91,11 +91,8 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI this.mobileAppService.getMobileAppSettings().subscribe((settings => { this.mobileAppSettings = settings; - const useDefaultApp = this.mobileAppSettings.useDefaultApp; - this.appStoreLink = useDefaultApp ? this.mobileAppSettings.defaultAppStoreLink : - this.mobileAppSettings.iosConfig.storeLink; - this.googlePlayLink = useDefaultApp ? this.mobileAppSettings.defaultGooglePlayLink : - this.mobileAppSettings.androidConfig.storeLink; + this.appStoreLink = this.mobileAppSettings.appStoreLink; + this.googlePlayLink = this.mobileAppSettings.googlePlayLink; if (isDefinedAndNotNull(this.ctx.settings.useSystemSettings) && !this.ctx.settings.useSystemSettings) { this.mobileAppSettings = mergeDeep(this.mobileAppSettings, this.ctx.settings); @@ -133,10 +130,8 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI clearTimeout(this.deepLinkTTLTimeoutID); } - navigateByDeepLink($event) { - if ($event) { - $event.stopPropagation(); - } + navigateByDeepLink($event: Event) { + $event?.stopPropagation(); if (this.ctx.isMobile) { window.open(this.deepLink, '_blank'); } @@ -157,8 +152,8 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI private updateQRCode(link: string) { import('qrcode').then((QRCode) => { - unwrapModule(QRCode).toString(link, (err, string) => { - this.qrCodeSVG = string; + unwrapModule(QRCode).toString(link, (_err, svgElement) => { + this.qrCodeSVG = svgElement; this.cd.markForCheck(); }) }); 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..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,8 +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'; @NgModule({ @@ -50,15 +48,13 @@ import { OAuth2Module } from '@home/pages/admin/oauth2/oauth2.module'; QueueComponent, RepositoryAdminSettingsComponent, AutoCommitAdminSettingsComponent, - TwoFactorAuthSettingsComponent, - MobileAppSettingsComponent + TwoFactorAuthSettingsComponent ], imports: [ CommonModule, SharedModule, HomeComponentsModule, AdminRoutingModule, - WidgetComponentsModule, OAuth2Module ] }) diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html deleted file mode 100644 index 51e7c6f43d..0000000000 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html +++ /dev/null @@ -1,201 +0,0 @@ - - - - - admin.mobile-app.mobile-app-qr-code-widget-settings - - - - -
- -
-
-
-
admin.mobile-app.applications
- - {{ 'admin.mobile-app.default' | translate }} - {{ '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 - - -
-
-
-
-
-
-
admin.mobile-app.appearance-on-home-page
- - {{ 'admin.mobile-app.enabled' | translate }} - {{ 'admin.mobile-app.disabled' | translate }} - -
-
-
- - {{ 'admin.mobile-app.badges' | translate }} - - - - - {{ badgePosition.value | translate }} - - - -
-
-
-
- - {{ 'admin.mobile-app.label' | translate }} - - - - - warning - - -
-
-
-
admin.mobile-app.preview
- - -
-
-
-
- -
-
-
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 deleted file mode 100644 index 7f2a48253f..0000000000 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts +++ /dev/null @@ -1,203 +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 { Component, OnDestroy } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { PageComponent } from '@shared/components/page.component'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; -import { Subject, takeUntil } from 'rxjs'; -import { MobileApplicationService } from '@core/http/mobile-application.service'; -import { - BadgePosition, - badgePositionTranslationsMap, - MobileAppSettings -} from '@shared/models/mobile-app.models'; -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'] -}) -export class MobileAppSettingsComponent extends PageComponent implements HasConfirmForm, OnDestroy { - - mobileAppSettingsForm: FormGroup; - - mobileAppSettings: MobileAppSettings; - - private readonly destroy$ = new Subject(); - - badgePositionTranslationsMap = badgePositionTranslationsMap; - - constructor(protected store: Store, - private mobileAppService: MobileApplicationService, - private fb: FormBuilder) { - super(store); - this.buildMobileAppSettingsForm(); - this.mobileAppService.getMobileAppSettings() - .subscribe(settings => this.processMobileAppSettings(settings)); - this.mobileAppSettingsForm.get('useDefaultApp').valueChanges.pipe( - 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}); - } 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('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 => { - if (value) { - this.mobileAppSettingsForm.get('qrCodeConfig').enable({emitEvent: false}); - } else { - this.mobileAppSettingsForm.get('qrCodeConfig').disable({emitEvent: false}); - this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').enable({emitEvent: false}); - } - this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').updateValueAndValidity({onlySelf: true}); - this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').updateValueAndValidity({onlySelf: true}); - }); - this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').valueChanges.pipe( - 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}); - } - } else { - this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); - } - }); - this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe(value => { - if (value && this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { - this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').enable({emitEvent: false}); - } else { - this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').disable({emitEvent: false}); - } - }); - } - - ngOnDestroy() { - super.ngOnDestroy(); - this.destroy$.next(); - this.destroy$.complete(); - } - - 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]] - }), - qrCodeConfig: this.fb.group({ - showOnHomePage: [true], - badgeEnabled: [true], - badgePosition: [BadgePosition.RIGHT], - qrCodeLabelEnabled: [true], - qrCodeLabel: ['', [Validators.required, Validators.maxLength(50)]] - }) - }); - } - - private processMobileAppSettings(mobileAppSettings: MobileAppSettings): void { - this.mobileAppSettings = {...mobileAppSettings}; - 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()}; - this.mobileAppService.saveMobileAppSettings(this.mobileAppSettings) - .subscribe((settings) => { - const showOnHomePageValue = settings.qrCodeConfig.showOnHomePage; - if (showOnHomePagePreviousValue !== showOnHomePageValue) { - this.store.dispatch(new ActionUpdateMobileQrCodeEnabled({mobileQrEnabled: showOnHomePageValue})); - } - this.processMobileAppSettings(settings); - }); - } - - confirmForm(): FormGroup { - return this.mobileAppSettingsForm; - } - -} 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 11077ab9ce..70355c0a3e 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() { 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..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 @@ -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() @@ -45,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' @@ -56,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' + } } }, { @@ -74,20 +76,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: { @@ -101,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: { @@ -119,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: ['../..'] @@ -140,8 +128,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 a2253f28e7..7757a1e490 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 @@ -45,6 +45,7 @@ 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 { GatewaysModule } from '@home/pages/gateways/gateways.module'; +import { MobileModule } from '@home/pages/mobile/mobile.module'; @NgModule({ exports: [ @@ -59,6 +60,7 @@ import { GatewaysModule } from '@home/pages/gateways/gateways.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..8e67f96a18 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts @@ -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. +/// + +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'; +import { MobileAppDialogComponent } from '@home/pages/mobile/applications/mobile-app-dialog.component'; +import { RemoveAppDialogComponent } from '@home/pages/mobile/applications/remove-app-dialog.component'; +import { CommonMobileModule } from '@home/pages/mobile/common/common-mobile.module'; + +@NgModule({ + declarations: [ + MobileAppComponent, + MobileAppTableHeaderComponent, + MobileAppDialogComponent, + RemoveAppDialogComponent, + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + CommonMobileModule, + ApplicationsRoutingModule, + ], + exports: [ + MobileAppDialogComponent, + ] +}) +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..80f48838ab --- /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-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..543ab89078 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts @@ -0,0 +1,195 @@ +/// +/// 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 { + CellActionDescriptor, + 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, + MobileAppBundleInfo, + MobileAppStatus, + mobileAppStatusTranslations +} from '@shared/models/mobile-app.models'; +import { platformTypeTranslations } from '@shared/models/oauth2.models'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { MatDialog } from '@angular/material/dialog'; +import { + MobileAppDeleteDialogData, + RemoveAppDialogComponent +} from '@home/pages/mobile/applications/remove-app-dialog.component'; + +@Injectable() +export class MobileAppTableConfigResolver { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private translate: TranslateService, + private datePipe: DatePipe, + private mobileAppService: MobileAppService, + 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); + 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.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('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 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(0, 148, 255, 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 = '#0094FF'; + break; + } + return styleObj; + } + +} 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 new file mode 100644 index 0000000000..40f1c5b02b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.html @@ -0,0 +1,26 @@ + +
+
+
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..17ca4ae7c7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.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{ + width: 100000px; +} 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..e74fb25b0b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html @@ -0,0 +1,187 @@ + +
+ + mobile.mobile-package + + + + + + {{ 'mobile.mobile-package-required' | translate }} + + + {{ 'mobile.mobile-package-max-length' | translate }} + + + {{ 'mobile.mobile-package-pattern' | translate }} + + + + mobile.platform-type + + + {{ platformTypeTranslations.get(platformType) | translate }} + + + + + mobile.application-secret + +
+ + + +
+ + + {{ 'mobile.application-secret-required' | translate }} + +
+ + mobile.status + + + {{ mobileAppStatusTranslations.get(mobileAppStatus) | translate }} + + + +
+
mobile.version-information
+
+
+ + mobile.min-version + + + + {{ 'mobile.invalid-version-pattern' | translate }} + + + +
+
+ + mobile.latest-version + + + + {{ 'mobile.invalid-version-pattern' | translate }} + + + +
+
+
+
+
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.invalid-store-link' | translate }} + + + + mobile.sha256-certificate-fingerprints + + + + + {{ 'mobile.sha256-certificate-fingerprints-required' | translate }} + + + {{ 'mobile.sha256-certificate-fingerprints-pattern' | translate }} + + + + mobile.app-id + + + + + {{ 'mobile.app-id-required' | translate }} + + + {{ 'mobile.app-id-pattern' | 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..98195c9c84 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts @@ -0,0 +1,201 @@ +/// +/// 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, Optional, 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'; +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'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { EditorPanelComponent } from '@home/pages/mobile/common/editor-panel.component'; + +@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, + @Optional() @Inject('entity') protected entityValue: MobileApp, + @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected cd: ChangeDetectorRef, + public fb: FormBuilder, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef) { + 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(/^[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 : '', 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 : '', 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 : '', + 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}$/)], + }), + }); + + 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}); + 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}); + }); + + form.get('status').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value: MobileAppStatus) => { + 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); + } else { + form.get('storeInfo.storeLink').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}); + 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('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}); + } 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(); + } + + 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), + title: isLatest ? 'mobile.latest-version-release-notes' : 'mobile.min-version-release-notes', + content: isLatest + ? this.entityForm.get('versionInfo.latestVersionReleaseNotes').value + : this.entityForm.get('versionInfo.minVersionReleaseNotes').value + }; + const releaseNotesPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, EditorPanelComponent, ['leftOnly', 'leftBottomOnly', 'leftTopOnly'], true, null, + ctx, + {}, + {}, {}, false, () => {}, {padding: '16px 24px'}); + releaseNotesPanelPopover.tbComponentRef.instance.popover = releaseNotesPanelPopover; + releaseNotesPanelPopover.tbComponentRef.instance.editorContentApplied.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; + } + 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/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/bundles-routing.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/bundles-routing.module.ts new file mode 100644 index 0000000000..d8dd749db1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/bundles-routing.module.ts @@ -0,0 +1,48 @@ +/// +/// 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 { Routes } from '@angular/router'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { MenuId } from '@core/services/menu.models'; +import { MobileBundleTableConfigResolver } from '@home/pages/mobile/bundes/mobile-bundle-table-config.resolve'; +import { NgModule } from '@angular/core'; + +export const bundlesRoutes: Routes = [ + { + path: 'bundles', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'mobile.bundles', + breadcrumb: { + menuId: MenuId.mobile_bundles + } + }, + resolve: { + entitiesTableConfig: MobileBundleTableConfigResolver + } + } +]; + +@NgModule({ + providers: [ + MobileBundleTableConfigResolver + ], + imports: [], + exports: [] +}) +export class MobileBundleRoutingModule { } 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 new file mode 100644 index 0000000000..c0385a8dd5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/bundles.module.ts @@ -0,0 +1,54 @@ +/// +/// 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 { MobileBundleRoutingModule } from '@home/pages/mobile/bundes/bundles-routing.module'; +import { MobileBundleTableHeaderComponent } from '@home/pages/mobile/bundes/mobile-bundle-table-header.component'; +import { MobileBundleDialogComponent } from '@home/pages/mobile/bundes/mobile-bundle-dialog.component'; +import { MobileLayoutComponent } from '@home/pages/mobile/bundes/layout/mobile-layout.component'; +import { MobilePageItemRowComponent } from '@home/pages/mobile/bundes/layout/mobile-page-item-row.component'; +import { AddMobilePageDialogComponent } from '@home/pages/mobile/bundes/layout/add-mobile-page-dialog.component'; +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: [ + MobileBundleTableHeaderComponent, + MobileBundleDialogComponent, + MobileLayoutComponent, + MobilePageItemRowComponent, + AddMobilePageDialogComponent, + CustomMobilePageComponent, + CustomMobilePagePanelComponent, + DefaultMobilePagePanelComponent, + MobileAppConfigurationDialogComponent, + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + MobileBundleRoutingModule, + ] +}) +export class MobileBundlesModule { } 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 new file mode 100644 index 0000000000..a4c888c8e8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.html @@ -0,0 +1,43 @@ + + +

mobile.add-specific-page

+ +
+
+ + +
+
+ + +
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..f543126660 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html @@ -0,0 +1,76 @@ + +
+
+ + {{ 'mobile.visible' | translate }} + +
+
+ + + + mobile.page-name + + +
+ + mobile.page-type + + + {{ mobilePageTypeTranslations.get(pageType) | translate }} + + + + + + + + + mobile.url + + + + {{ 'mobile.url-pattern' | translate }} + + + + + + mobile.path + + + + {{ 'mobile.path-pattern' | translate }} + + + +
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..641e7c877d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.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{ + --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); + + .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..8d2b0cc675 --- /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, Validators.pattern(/^(https?:\/\/)?(localhost|([\w\-]+\.)+[\w\-]+)(:\d+)?(\/[\w\-._~:\/?#[\]@!$&'()*+,;=%]*)?$/)]], + path: [{value:'', disabled: true}, [Validators.required, Validators.pattern(/^(\/[\w\-._~:\/?#[\]@!$&'()*+,;=%]*)?$/)]] + }); + + 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..3ed1601a03 --- /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..875e7d8703 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.scss @@ -0,0 +1,56 @@ +/** + * 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; + --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); + @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..2bf38a94f1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html @@ -0,0 +1,105 @@ + +
+
+
+ mobile.pages + {{ 'mobile.show-hidden-pages' | translate }} +
+ + + + + + +
+
+
+
+
+ {{ '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..4a26509b04 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.scss @@ -0,0 +1,184 @@ +/** + * 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 { + .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..2b561a4fce --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.ts @@ -0,0 +1,250 @@ +/// +/// 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; + + showHiddenPages = new FormControl(true); + + 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(); + if (this.showHiddenPages.value) { + this.showHiddenPages.setValue(false); + } + } + + 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.filter(c => this.showHiddenPages.value || c.value.visible); + } + + 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..eeec353103 --- /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-app-configuration-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.html new file mode 100644 index 0000000000..17108aafe2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.html @@ -0,0 +1,79 @@ + + +

mobile.configuration-dialog

+ +
+
+
+
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}} + + +
+ diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.scss new file mode 100644 index 0000000000..721a3e8719 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.scss @@ -0,0 +1,96 @@ +/** + * 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 { + height: 100%; + max-height: 100vh; + display: grid; + width: 870px; + max-width: 100%; + grid-template-rows: min-content minmax(auto, 1fr) min-content; +} + +:host-context(.mat-mdc-dialog-container) { + .tb-dialog-actions { + padding: 8px 16px 8px 24px; + } +} + +:host ::ng-deep { + .tb-markdown-view { + .tb-command-code { + .code-wrapper { + padding: 0; + pre[class*=language-] { + margin: 0; + font-size: 14px; + background: #F3F6FA; + border-color: $tb-primary-color; + padding-right: 38px; + overflow: scroll; + padding-bottom: 4px; + min-height: 42px; + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: 4px; + height: 4px; + } + } + } + button.clipboard-btn { + right: -2px; + p { + color: $tb-primary-color; + } + p, div { + background-color: #F3F6FA; + } + div { + img { + display: none; + } + &:after { + content: ""; + position: initial; + display: block; + width: 18px; + height: 18px; + background: $tb-primary-color; + mask-image: url(/src/assets/copy-code-icon.svg); + -webkit-mask-image: url(/src/assets/copy-code-icon.svg); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + } + } + } + } + } + .mdc-button__label > span { + .mat-icon { + vertical-align: text-bottom; + box-sizing: initial; + } + } + + .tabs-icon { + margin-right: 8px; + } + + .tb-form-panel.tb-tab-body { + padding: 16px 0 0; + } +} 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 new file mode 100644 index 0000000000..897710def9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts @@ -0,0 +1,85 @@ +/// +/// 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 { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions'; + +export interface MobileAppConfigurationDialogData { + afterAdd: boolean; + appSecret: string; +} + +@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; + + gitRepositoryLink = 'git clone -b master https://github.com/thingsboard/flutter_thingsboard_app.git'; + pathToConstants = 'lib/constants/app_constants.dart'; + flutterRunCommand = 'flutter run'; + + configureApi: string[] = []; + + 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; + + this.configureApi.push(`static const thingsBoardApiEndpoint = '${window.location.origin}';`); + this.configureApi.push(`static const thingsboardOAuth2AppSecret = '${this.data.appSecret}';`); + } + + close(): void { + if (this.notShowAgain && this.showDontShowAgain) { + this.store.dispatch(new ActionPreferencesPutUserSettings({ notDisplayConfigurationAfterAddMobileBundle: true })); + this.dialogRef.close(null); + } else { + this.dialogRef.close(null); + } + } + + createMarkDownCommand(commands: string | string[]): string { + if (Array.isArray(commands)) { + const formatCommands: Array = []; + commands.forEach(command => formatCommands.push(this.createMarkDownSingleCommand(command))); + return formatCommands.join(`\n
\n\n`); + } else { + return this.createMarkDownSingleCommand(commands); + } + } + + private createMarkDownSingleCommand(command: string): string { + return '```bash\n' + + command + + '{:copy-code}\n' + + '```'; + } +} 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..8e029cf9f7 --- /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..a93cd1026e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.scss @@ -0,0 +1,74 @@ +/** + * 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; + --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); + + .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..6e43e1f650 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts @@ -0,0 +1,229 @@ +/// +/// 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] + }); + + isAdd = false; + + 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.isAdd = true; + } + + 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..c1c00b40ac --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts @@ -0,0 +1,199 @@ +/// +/// 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, take } 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 { + MobileAppConfigurationDialogComponent, + MobileAppConfigurationDialogData +} from '@home/pages/mobile/bundes/mobile-app-configuration-dialog.component'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { selectUserSettingsProperty } from '@core/auth/auth.selectors'; +import { forkJoin, of } from 'rxjs'; + +@Injectable() +export class MobileBundleTableConfigResolver { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor( + private datePipe: DatePipe, + private mobileAppService: MobileAppService, + private translate : TranslateService, + private dialog: MatDialog, + private store: Store + ) { + 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; + }; + + 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) + } + ]; + } + + 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 && !isAdd) { + this.config.updateData(); + } else { + this.store.pipe(select(selectUserSettingsProperty('notDisplayConfigurationAfterAddMobileBundle'))).pipe( + take(1) + ).subscribe((settings: boolean) => { + if (!settings) { + this.configurationApp(null, res, true); + } else { + this.config.updateData(); + } + }); + } + }); + } + + private onBundleAction(action: EntityAction): boolean { + switch (action.action) { + case 'add': + this.editBundle(action.event, action.entity, true); + return true; + } + return false; + } + + private configurationApp($event: Event, entity: MobileAppBundleInfo, afterAdd = false) { + if ($event) { + $event.stopPropagation(); + } + const task = { + androidApp: entity.androidAppId ? this.mobileAppService.getMobileAppInfoById(entity.androidAppId.id) : of(null), + iosApp: entity.iosAppId ? this.mobileAppService.getMobileAppInfoById(entity.iosAppId.id) : of(null) + }; + forkJoin(task).subscribe(data => { + this.dialog.open(MobileAppConfigurationDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + afterAdd, + appSecret: data.androidApp?.appSecret || data.iosApp?.appSecret + } + }).afterClosed() + .subscribe(() => { + if (afterAdd) { + this.config.updateData(); + } + }); + }) + } +} 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/bundes/mobile-bundle-table-header.component.html similarity index 61% 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/bundes/mobile-bundle-table-header.component.html index fd66a026da..bf2a571e98 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/bundes/mobile-bundle-table-header.component.html @@ -15,4 +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 new file mode 100644 index 0000000000..17ca4ae7c7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.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{ + width: 100000px; +} 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/common/common-mobile.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/common/common-mobile.module.ts new file mode 100644 index 0000000000..4ea48b6cc1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/common/common-mobile.module.ts @@ -0,0 +1,34 @@ +/// +/// 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 { EditorPanelComponent } from '@home/pages/mobile/common/editor-panel.component'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; + +@NgModule({ + declarations: [ + EditorPanelComponent + ], + imports: [ + CommonModule, + SharedModule + ], + exports: [ + EditorPanelComponent + ] +}) +export class CommonMobileModule {} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.html b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.html new file mode 100644 index 0000000000..d2ff37f0a1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.html @@ -0,0 +1,36 @@ + +
+
{{ title | translate }}
+ +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.scss new file mode 100644 index 0000000000..00a2ebca2d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.scss @@ -0,0 +1,44 @@ +/** + * 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-editor-panel { + width: 600px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-editor-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-editor { + height: 400px; + } + .tb-editor-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/common/editor-panel.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts new file mode 100644 index 0000000000..966ebd6411 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts @@ -0,0 +1,79 @@ +/// +/// 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: './editor-panel.component.html', + styleUrls: ['./editor-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class EditorPanelComponent implements OnInit { + + @Input() + disabled: boolean; + + @Input() + content: string; + + @Input() + title: string; + + @Input() + popover: TbPopoverComponent; + + @Output() + editorContentApplied = new EventEmitter(); + + editorControl: 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.editorControl = this.fb.control(this.content); + if (this.disabled) { + this.editorControl.disable({emitEvent: false}); + } + } + + cancel() { + this.popover?.hide(); + } + + apply() { + if (this.editorControl.valid) { + this.editorContentApplied.emit(this.editorControl.value); + } + } +} 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..654fc2c730 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/mobile-routing.module.ts @@ -0,0 +1,56 @@ +/// +/// 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 { 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 = [ + { + 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/bundles' + } + }, + ...applicationsRoutes, + ...bundlesRoutes, + ...qrCodeWidgetRoutes + ] + } +]; + +@NgModule({ + 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..49f59aa147 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/mobile.module.ts @@ -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. +/// + +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 { 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: [ + CommonModule, + SharedModule, + HomeComponentsModule, + MobileApplicationModule, + MobileBundlesModule, + MobileQrCodeWidgetSettingsModule, + MobileRoutingModule, + ] +}) +export class MobileModule { } 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..bc037ba94d --- /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,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 { 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 new file mode 100644 index 0000000000..4005c60178 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html @@ -0,0 +1,120 @@ + + + + + admin.mobile-app.mobile-app-qr-code-widget-settings + +
+
+ + +
+ +
+
+
+
admin.mobile-app.applications
+ + {{ 'admin.mobile-app.default' | translate }} + {{ 'admin.mobile-app.custom' | translate }} + +
+
+
{{ 'mobile.bundle' | translate }}
+ + +
+
+ + {{ 'admin.mobile-app.android' | translate }} + +
+
+ + {{ 'admin.mobile-app.ios' | translate }} + +
+
+
+
+
admin.mobile-app.appearance-on-home-page
+ + {{ 'admin.mobile-app.enabled' | translate }} + {{ 'admin.mobile-app.disabled' | translate }} + +
+
+
+ + {{ 'admin.mobile-app.badges' | translate }} + + + + + {{ badgePosition.value | translate }} + + + +
+
+
+
+ + {{ 'admin.mobile-app.label' | translate }} + + + + + warning + + +
+
+
+
admin.mobile-app.preview
+ + +
+
+
+
+ +
+
+
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-qr-code-widget-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-qr-code-widget-settings.component.scss 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 new file mode 100644 index 0000000000..3ddb15cd3b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts @@ -0,0 +1,140 @@ +/// +/// 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 { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; +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 { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +@Component({ + 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 MobileQrCodeWidgetSettingsComponent extends PageComponent implements HasConfirmForm { + + readonly badgePositionTranslationsMap = badgePositionTranslationsMap; + readonly entityType = EntityType; + + mobileAppSettingsForm = this.fb.group({ + 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 mobileAppSettings: QrCodeSettings; + + constructor(protected store: Store, + private mobileAppService: MobileApplicationService, + private fb: FormBuilder) { + super(store); + this.mobileAppService.getMobileAppSettings() + .subscribe(settings => this.processMobileAppSettings(settings)); + this.mobileAppSettingsForm.get('useDefaultApp').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(value => { + if (value) { + this.mobileAppSettingsForm.get('mobileAppBundleId').disable({emitEvent: false}); + } else { + this.mobileAppSettingsForm.get('mobileAppBundleId').enable({emitEvent: false}); + } + }); + 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 => { + if (value) { + this.mobileAppSettingsForm.get('qrCodeConfig').enable({emitEvent: false}); + } else { + this.mobileAppSettingsForm.get('qrCodeConfig').disable({emitEvent: false}); + this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').enable({emitEvent: false}); + } + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').updateValueAndValidity({onlySelf: true}); + this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').updateValueAndValidity({onlySelf: true}); + }); + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(value => { + if (value) { + 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}); + } + }); + this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(value => { + if (value && this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { + this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').enable({emitEvent: false}); + } else { + this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').disable({emitEvent: false}); + } + }); + } + + private processMobileAppSettings(mobileAppSettings: QrCodeSettings): void { + this.mobileAppSettings = {...mobileAppSettings}; + this.mobileAppSettingsForm.reset(this.mobileAppSettings); + } + + save(): void { + const showOnHomePagePreviousValue = this.mobileAppSettings.qrCodeConfig.showOnHomePage; + this.mobileAppSettings = {...this.mobileAppSettings, ...this.mobileAppSettingsForm.getRawValue()}; + this.mobileAppService.saveMobileAppSettings(this.mobileAppSettings) + .subscribe((settings) => { + const showOnHomePageValue = settings.qrCodeConfig.showOnHomePage; + if (showOnHomePagePreviousValue !== showOnHomePageValue) { + this.store.dispatch(new ActionUpdateMobileQrCodeEnabled({mobileQrEnabled: showOnHomePageValue})); + } + this.processMobileAppSettings(settings); + }); + } + + 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..597e67d1fc --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.module.ts @@ -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. +/// + +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 8986ed12e9..c194e04290 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 }} 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..22d41e2cdc 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 @@ -25,7 +25,7 @@ import { Output, ViewChild } from '@angular/core'; -import { MatFormFieldAppearance } from '@angular/material/form-field'; +import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { merge, Observable, of, Subject } from 'rxjs'; import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/operators'; @@ -39,7 +39,7 @@ import { EntityService } from '@core/http/entity.service'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; import { getEntityDetailsPageURL, isDefinedAndNotNull, isEqual } from '@core/utils'; -import { coerceBoolean } from '@shared/decorators/coercion'; +import { coerceArray, coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-entity-autocomplete', @@ -131,9 +131,23 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit @coerceBoolean() disabled: boolean; + @Input() + @coerceBoolean() + allowCreateNew: boolean; + + @Input() + subscriptSizing: SubscriptSizing = 'fixed'; + + @Input() + @coerceArray() + additionalClasses: Array; + @Output() entityChanged = new EventEmitter>(); + @Output() + createNew = new EventEmitter(); + @ViewChild('entityInput', {static: true}) entityInput: ElementRef; get requiredErrorText(): string { @@ -266,6 +280,18 @@ 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.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'; @@ -417,4 +443,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 ce55412ad7..ee11801a03 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/id/mobile-app-bundle-id.ts b/ui-ngx/src/app/shared/models/id/mobile-app-bundle-id.ts new file mode 100644 index 0000000000..000dccd62d --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/mobile-app-bundle-id.ts @@ -0,0 +1,27 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class MobileAppBundleId implements EntityId { + entityType = EntityType.MOBILE_APP_BUNDLE; + id: string; + + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/public-api.ts b/ui-ngx/src/app/shared/models/id/public-api.ts index ea97c51ca0..ebaaf86760 100644 --- a/ui-ngx/src/app/shared/models/id/public-api.ts +++ b/ui-ngx/src/app/shared/models/id/public-api.ts @@ -26,6 +26,8 @@ export * from './entity-id'; export * from './entity-view-id'; export * from './event-id'; export * from './has-uuid'; +export * from './mobile-app-bundle-id'; +export * from './mobile-app-id'; export * from './notification-id'; export * from './notification-request-id'; export * from './notification-rule-id'; 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 b879a9eb45..acf609a637 100644 --- a/ui-ngx/src/app/shared/models/mobile-app.models.ts +++ b/ui-ngx/src/app/shared/models/mobile-app.models.ts @@ -15,27 +15,23 @@ /// import { HasTenantId } from '@shared/models/entity.models'; +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 MobileAppSettings extends HasTenantId { +export interface QrCodeSettings extends HasTenantId { useDefaultApp: boolean; - androidConfig: AndroidConfig; - iosConfig: IosConfig; + mobileAppBundleId: MobileAppBundleId + androidEnabled: boolean; + iosEnabled: boolean; qrCodeConfig: QRCodeConfig; - defaultGooglePlayLink: string; - defaultAppStoreLink: string; -} - -export interface AndroidConfig { - enabled: boolean; - appPackage: string; - sha256CertFingerprints: string; - storeLink: string; -} - -export interface IosConfig { - enabled: boolean; - appId: string; - storeLink: string; + readonly googlePlayLink: string; + readonly appStoreLink: string; + id: { + id: string; + } } export interface QRCodeConfig { @@ -46,11 +42,6 @@ export interface QRCodeConfig { qrCodeLabel: string; } -export interface MobileOSBadgeURL { - iOS: string; - android: string; -} - export enum BadgePosition { RIGHT = 'RIGHT', LEFT = 'LEFT' @@ -60,3 +51,252 @@ export const badgePositionTranslationsMap = new Map([ [BadgePosition.RIGHT, 'admin.mobile-app.right'], [BadgePosition.LEFT, 'admin.mobile-app.left'] ]); + +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', + NOTIFICATIONS = 'NOTIFICATIONS' +} + +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 { + pages: MobilePage[]; +} + +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; + 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/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/app/shared/models/user-settings.models.ts b/ui-ngx/src/app/shared/models/user-settings.models.ts index bd2b675c98..9a70f07883 100644 --- a/ui-ngx/src/app/shared/models/user-settings.models.ts +++ b/ui-ngx/src/app/shared/models/user-settings.models.ts @@ -18,6 +18,7 @@ export interface UserSettings { openedMenuSections?: string[]; notDisplayConnectivityAfterAddDevice?: boolean; notDisplayInstructionsAfterAddEdge?: boolean; + notDisplayConfigurationAfterAddMobileBundle?: boolean; includeBundleWidgetsInExport?: boolean; } 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 d3894fe989..122f7807a2 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", @@ -463,17 +464,7 @@ "default": "Default", "custom": "Custom", "android": "Android", - "app-package-name": "App package name", - "app-package-name-required": "App package name is required", - "sha256-certificate-fingerprints": "SHA256 certificate fingerprints", - "sha256-certificate-fingerprints-required": "SHA256 certificate fingerprints is required", "ios": "iOS", - "app-id": "App ID", - "app-id-required": "App ID is required", - "google-play-link": "Google Play link", - "google-play-link-required": "Google Play link is required", - "app-store-link": "App Store link", - "app-store-link-required": "App Store link is required", "appearance": "Appearance", "appearance-on-home-page": "Appearance on Home page", "enabled": "Enabled", @@ -2383,7 +2374,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", @@ -3424,6 +3418,130 @@ "copy-code": "Click to copy", "copied": "Copied!" }, + "mobile": { + "add-application": "Add application", + "app-id": "App ID", + "app-id-required": "App ID is required", + "app-id-pattern": "Invalid format App ID", + "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", + "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", + "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-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", + "google-play-link": "Google Play link", + "google-play-link-required": "Google Play link is required", + "latest-version": "Latest version", + "min-version": "Min version", + "invalid-version-pattern": "Invalid version format. Please use the format: major.minor.patch (e.g., 1.0.0).", + "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-pattern": "Application package invalid format", + "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", + "sha256-certificate-fingerprints-pattern": "Invalid format SHA256 certificate fingerprint", + "show-hidden-pages": "Show hidden pages", + "status": "Status", + "status-type": { + "deprecated": "Deprecated", + "draft": "Draft", + "published": "Published", + "suspended": "Suspended" + }, + "store-information": "Store information", + "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", + "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", + "invalid-store-link": "Invalid store link", + "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.", + "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", + "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", + "url-pattern": "Invalid URL", + "path": "Path", + "path-pattern": "Path pattern", + "custom-page": "Custom page", + "edit-page": "Edit page", + "edit-custom-page": "Edit custom page", + "delete-page": "Delete page", + "qr-code-widget": "QR code widget", + "type-here": "Type hero", + "configuration-dialog": "Configuration dialog", + "configuration-app": "Configuration app", + "configuration-step": { + "prepare-environment-title": "Prepare development environment", + "prepare-environment-text": "Flutter ThingsBoard Mobile Application requires Flutter SDK. Follow instructions to set up Flutter SDK.", + "get-source-code-title": "Get app source code", + "get-source-code-text": "You can get Flutter ThingsBoard Mobile Application source code by cloning it from the GitHub repository:", + "configure-api-title": "Configure ThingsBoard API endpoint", + "configure-api-text": "Open the flutter_thingsboard_app project in your editor/IDE. Edit:", + "configure-api-hint": "Set the value of the thingsBoardApiEndpoint constant to match the API endpoint of your ThingsBoard server instance. Do not use “localhost” or “127.0.0.1” hostnames.", + "run-app-title": "Run the app", + "run-app-text": "Run the app as described in your IDE.\nIf using the terminal, run the app with the following command:", + "more-information": "Detailed information may be found in our Getting Started documentation.", + "getting-started": "Getting Started" + } + }, "notification": { "action-button": "Action button", "action-type": "Action type", diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 0d1390c360..346941d4d1 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -907,6 +907,9 @@ pre.tb-highlight { &.tb-mat-20 { @include tb-mat-icon-size(20); } + &.tb-mat-24 { + @include tb-mat-icon-size(24); + } &.tb-mat-28 { @include tb-mat-icon-size(28); }