Browse Source

moved all apis to one controller

pull/10591/head
dashevchenko 2 years ago
parent
commit
67ba644195
  1. 8
      application/src/main/data/upgrade/3.6.4/schema_update.sql
  2. 31
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  3. 86
      application/src/main/java/org/thingsboard/server/controller/MobileAppLinksController.java
  4. 173
      application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java
  5. 94
      application/src/main/java/org/thingsboard/server/controller/QRCodeController.java
  6. 4
      application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretService.java
  7. 14
      application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretServiceImpl.java
  8. 6
      application/src/main/java/org/thingsboard/server/service/qr/MobileSecretCaffeineCache.java
  9. 2
      application/src/main/java/org/thingsboard/server/service/qr/MobileSecretEvictEvent.java
  10. 6
      application/src/main/java/org/thingsboard/server/service/qr/MobileSecretRedisCache.java
  11. 4
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  12. 6
      application/src/main/resources/thingsboard.yml
  13. 24
      application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java
  14. 132
      application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java
  15. 1
      application/src/test/resources/application-test.properties
  16. 2
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  17. 38
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java
  18. 23
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java
  19. 24
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgeStyle.java
  20. 36
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java
  21. 41
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java
  22. 38
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java
  23. 4
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java
  24. 43
      dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java
  25. 2
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java
  26. 8
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  27. 54
      dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java
  28. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java
  29. 8
      dao/src/main/resources/sql/schema-entities.sql

8
application/src/main/data/upgrade/3.6.4/schema_update.sql

@ -139,10 +139,10 @@ DELETE FROM asset_profile WHERE name ='TbServiceQueue';
CREATE TABLE IF NOT EXISTS mobile_app_settings (
tenant_id UUID NOT NULL,
app_package VARCHAR(100) UNIQUE,
sha256_cert_fingerprints VARCHAR(10000),
app_id VARCHAR(100) UNIQUE,
settings VARCHAR(100000)
use_default boolean,
android_config VARCHAR(1000),
ios_config VARCHAR(1000),
qr_code_config VARCHAR(100000)
);
-- MOBILE APPS TABLE CREATE END

31
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -65,7 +65,6 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
@ -76,7 +75,6 @@ import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo;
import org.thingsboard.server.common.data.sync.vc.VcUtils;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.mobile.MobileAppSettingsService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
@ -97,7 +95,6 @@ import java.util.Optional;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent
@ -122,7 +119,6 @@ public class AdminController extends BaseController {
private final UpdateService updateService;
private final SystemInfoService systemInfoService;
private final AuditLogService auditLogService;
private final MobileAppSettingsService mobileAppSettingsService;
@ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)",
notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@ -486,31 +482,4 @@ public class AdminController extends BaseController {
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings);
response.sendRedirect(prevUri);
}
@ApiOperation(value = "Create Or Update the Mobile application settings (saveMobileAppSettings)",
notes = "The payload contains platform qr code widget settings and associated android and iOS applications." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/mobileAppSettings", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public MobileAppSettings saveMobileAppSettings(@Parameter(description = "A JSON value representing the mobile apps configuration")
@RequestBody MobileAppSettings mobileAppSettings) throws ThingsboardException {
SecurityUser currentUser = getCurrentUser();
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.WRITE);
mobileAppSettings.setTenantId(getTenantId());
return mobileAppSettingsService.saveMobileAppSettings(currentUser.getTenantId(), mobileAppSettings);
}
@ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)",
notes = "The payload contains platform qr code widget settings and creds for associated android and iOS applications." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/mobileAppSettings", method = RequestMethod.GET)
@ResponseBody
public MobileAppSettings getMobileAppSettings() throws ThingsboardException {
SecurityUser currentUser = getCurrentUser();
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ);
return mobileAppSettingsService.getMobileAppSettings(currentUser.getTenantId());
}
}

86
application/src/main/java/org/thingsboard/server/controller/MobileAppLinksController.java

@ -1,86 +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.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.mobile.MobileAppSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@RequiredArgsConstructor
@RestController
@TbCoreComponent
public class MobileAppLinksController extends BaseController {
public static final String ASSET_LINKS_PATTERN = "[{\n" +
" \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n" +
" \"target\": {\n" +
" \"namespace\": \"android_app\",\n" +
" \"package_name\": \"%s\",\n" +
" \"sha256_cert_fingerprints\":\n" +
" [\"%s\"]\n" +
" }\n" +
"}]";
public static final String APPLE_APP_SITE_ASSOCIATION_PATTERN = "{\n" +
" \"applinks\": {\n" +
" \"apps\": [],\n" +
" \"details\": [\n" +
" {\n" +
" \"appID\": \"%s\",\n" +
" \"paths\": [ \"*\" ]\n" +
" }\n" +
" ]\n" +
" }\n" +
"}";
private final MobileAppSettingsService mobileAppSettingsService;
@ApiOperation(value = "Get associated android applications (getAssetLinks)")
@RequestMapping(value = "/.well-known/assetlinks.json", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<JsonNode> getAssetLinks() {
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
if (mobileAppSettings != null && mobileAppSettings.getAppPackage() != null && mobileAppSettings.getSha256CertFingerprints() != null) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, mobileAppSettings.getAppPackage(), mobileAppSettings.getSha256CertFingerprints())));
} else {
return ResponseEntity.notFound().build();
}
}
@ApiOperation(value = "Get associated ios applications (getAppleAppSiteAssociation)")
@RequestMapping(value = "/.well-known/apple-app-site-association", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<JsonNode> getAppleAppSiteAssociation() {
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
if (mobileAppSettings != null && mobileAppSettings.getAppId() != null) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, mobileAppSettings.getAppId())));
} else {
return ResponseEntity.notFound().build();
}
}
}

173
application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java

@ -0,0 +1,173 @@
/**
* 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 io.swagger.v3.oas.annotations.Parameter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
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.RequestBody;
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.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
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.security.model.JwtPair;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.mobile.MobileAppSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.qr.MobileAppSecretService;
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 org.thingsboard.server.service.security.system.SystemSecurityService;
import java.net.URI;
import java.net.URISyntaxException;
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 {
@Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}")
private int mobileSecretKeyTtl;
public static final String ASSET_LINKS_PATTERN = "[{\n" +
" \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n" +
" \"target\": {\n" +
" \"namespace\": \"android_app\",\n" +
" \"package_name\": \"%s\",\n" +
" \"sha256_cert_fingerprints\":\n" +
" [\"%s\"]\n" +
" }\n" +
"}]";
public static final String APPLE_APP_SITE_ASSOCIATION_PATTERN = "{\n" +
" \"applinks\": {\n" +
" \"apps\": [],\n" +
" \"details\": [\n" +
" {\n" +
" \"appID\": \"%s\",\n" +
" \"paths\": [ \"/api/noauth/qr\" ]\n" +
" }\n" +
" ]\n" +
" }\n" +
"}";
public static final String SECRET = "secret";
public static final String SECRET_PARAM_DESCRIPTION = "A string value representing short-live secret key";
public static final String DEFAULT_APP_DOMAIN = "demo.thingsboard.io";
public static final String DEEP_LINK_PATTERN = "https://%s/api/noauth/qr?secret=%s&ttl=%s";
private final SystemSecurityService systemSecurityService;
private final MobileAppSecretService mobileAppSecretService;
private final MobileAppSettingsService mobileAppSettingsService;
@ApiOperation(value = "Get associated android applications (getAssetLinks)")
@GetMapping(value = "/.well-known/assetlinks.json")
public ResponseEntity<JsonNode> getAssetLinks() {
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
AndroidConfig androidConfig = mobileAppSettings.getAndroidConfig();
if (androidConfig != null && androidConfig.isEnabled() && !androidConfig.getAppPackage().isBlank() && !androidConfig.getSha256CertFingerprints().isBlank()) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, androidConfig.getAppPackage(), androidConfig.getSha256CertFingerprints())));
} else {
return ResponseEntity.notFound().build();
}
}
@ApiOperation(value = "Get associated ios applications (getAppleAppSiteAssociation)")
@GetMapping(value = "/.well-known/apple-app-site-association")
public ResponseEntity<JsonNode> getAppleAppSiteAssociation() {
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
IosConfig iosConfig = mobileAppSettings.getIosConfig();
if (iosConfig != null && iosConfig.isEnabled() && !iosConfig.getAppId().isBlank()) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, iosConfig.getAppId())));
} else {
return ResponseEntity.notFound().build();
}
}
@ApiOperation(value = "Create Or Update the Mobile application settings (saveMobileAppSettings)",
notes = "The payload contains associated android and 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 {
SecurityUser currentUser = getCurrentUser();
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.WRITE);
mobileAppSettings.setTenantId(getTenantId());
return mobileAppSettingsService.saveMobileAppSettings(currentUser.getTenantId(), mobileAppSettings);
}
@ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)",
notes = "The payload contains associated android and 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 {
SecurityUser currentUser = getCurrentUser();
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ);
return mobileAppSettingsService.getMobileAppSettings(currentUser.getTenantId());
}
@ApiOperation(value = "Get the deep link to the associated mobile application (getMobileAppDeepLink)",
notes = "Fetch the url that takes user to associated mobile application " + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/api/mobile/deepLink", produces = "text/plain")
public String getMobileAppDeepLink(HttpServletRequest request) throws ThingsboardException, URISyntaxException {
SecurityUser currentUser = getCurrentUser();
String secret = mobileAppSecretService.generateMobileAppSecret(currentUser);
String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request);
String platformDomain = new URI(baseUrl).getHost();
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
String appDomain;
if (!mobileAppSettings.isUseDefault()) {
appDomain = platformDomain;
} else {
appDomain = DEFAULT_APP_DOMAIN;
}
String deepLink = String.format(DEEP_LINK_PATTERN, appDomain, secret, mobileSecretKeyTtl);
if (!appDomain.equals(platformDomain)) {
deepLink = deepLink + "&host=" + baseUrl;
}
return deepLink;
}
@ApiOperation(value = "Get User Token (getUserTokenByMobileSecret)",
notes = "Returns the token of the User based on the provided secret key.")
@GetMapping(value = "/api/noauth/qr/{secret}")
public JwtPair getUserTokenByMobileSecret(@Parameter(description = SECRET_PARAM_DESCRIPTION)
@PathVariable(SECRET) String secret) throws ThingsboardException {
checkParameter(SECRET, secret);
return mobileAppSecretService.getJwtPair(secret);
}
}

94
application/src/main/java/org/thingsboard/server/controller/QRCodeController.java

@ -1,94 +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 io.swagger.v3.oas.annotations.Parameter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
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.queue.util.TbCoreComponent;
import org.thingsboard.server.service.qr.QRService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.net.URI;
import java.net.URISyntaxException;
@RequiredArgsConstructor
@RestController
@TbCoreComponent
@RequestMapping("/api")
public class QRCodeController extends BaseController {
public static final String SECRET = "secret";
public static final String SECRET_PARAM_DESCRIPTION = "A string value representing short-live secret key";
public static final String DEFAULT_APP_DOMAIN = "demo.thingsboard.io";
public static final String DEEP_LINK_PATTERN = "https://%s/api/noauth/qr?secret=%s";
private final QRService qrService;
private final SystemSecurityService systemSecurityService;
private final MobileAppSettingsService mobileAppSettingsService;
@ApiOperation(value = "Get the deep link to the associated mobile application (getQRCodeDeepLink)",
notes = "Fetch the url that takes user to associated mobile application ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/qr/deepLink", method = RequestMethod.GET, produces = "text/plain")
@ResponseBody
public String getQRCodeDeepLink(HttpServletRequest request) throws ThingsboardException, URISyntaxException {
SecurityUser currentUser = getCurrentUser();
String secret = qrService.generateSecret(currentUser);
String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request);
String platformDomain = new URI(baseUrl).getHost();
JsonNode mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID).getSettings();
String appDomain;
if (mobileAppSettings != null && mobileAppSettings.get("useDefault") != null
&& !mobileAppSettings.get("useDefault").asBoolean()) {
appDomain = platformDomain;
} else {
appDomain = DEFAULT_APP_DOMAIN;
}
String deepLink = String.format(DEEP_LINK_PATTERN, appDomain, secret);
if (!appDomain.equals(platformDomain)) {
deepLink = deepLink + "&host=" + baseUrl;
}
return deepLink;
}
@ApiOperation(value = "Get User Token (getUserToken)",
notes = "Returns the token of the User based on the provided secret key.")
@RequestMapping(value = "/noauth/qr/{secret}", method = RequestMethod.GET)
@ResponseBody
public JwtPair getUserToken(@Parameter(description = SECRET_PARAM_DESCRIPTION)
@PathVariable(SECRET) String secret) throws ThingsboardException {
checkParameter(SECRET, secret);
return qrService.getJwtPair(secret);
}
}

4
application/src/main/java/org/thingsboard/server/service/qr/QRService.java → application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretService.java

@ -19,9 +19,9 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface QRService {
public interface MobileAppSecretService {
String generateSecret(SecurityUser securityUser);
String generateMobileAppSecret(SecurityUser securityUser);
JwtPair getJwtPair(String secret) throws ThingsboardException;

14
application/src/main/java/org/thingsboard/server/service/qr/QRServiceImpl.java → application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretServiceImpl.java

@ -29,21 +29,21 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import static org.thingsboard.server.service.security.system.DefaultSystemSecurityService.DEFAULT_QR_SECRET_KEY_LENGTH;
import static org.thingsboard.server.service.security.system.DefaultSystemSecurityService.DEFAULT_MOBILE_SECRET_KEY_LENGTH;
@Service
@Slf4j
@RequiredArgsConstructor
public class QRServiceImpl extends AbstractCachedService<String, JwtPair, QRSecretEvictEvent> implements QRService {
public class MobileAppSecretServiceImpl extends AbstractCachedService<String, JwtPair, MobileSecretEvictEvent> implements MobileAppSecretService {
private final JwtTokenFactory tokenFactory;
private final SystemSecurityService systemSecurityService;
@Override
public String generateSecret(SecurityUser securityUser) {
public String generateMobileAppSecret(SecurityUser securityUser) {
log.trace("Executing generateSecret for user [{}]", securityUser.getId());
Integer qrSecretKeyLength = systemSecurityService.getSecuritySettings().getQrSecretKeyLength();
String secret = StringUtils.generateSafeToken(qrSecretKeyLength == null ? DEFAULT_QR_SECRET_KEY_LENGTH : qrSecretKeyLength);
Integer mobileSecretKeyLength = systemSecurityService.getSecuritySettings().getMobileSecretKeyLength();
String secret = StringUtils.generateSafeToken(mobileSecretKeyLength == null ? DEFAULT_MOBILE_SECRET_KEY_LENGTH : mobileSecretKeyLength);
cache.put(secret, tokenFactory.createTokenPair(securityUser));
return secret;
}
@ -58,9 +58,9 @@ public class QRServiceImpl extends AbstractCachedService<String, JwtPair, QRSecr
}
}
@TransactionalEventListener(classes = QRSecretEvictEvent.class)
@TransactionalEventListener(classes = MobileSecretEvictEvent.class)
@Override
public void handleEvictEvent(QRSecretEvictEvent event) {
public void handleEvictEvent(MobileSecretEvictEvent event) {
cache.evict(event.getSecret());
}
}

6
application/src/main/java/org/thingsboard/server/service/qr/QRSecretCaffeineCache.java → application/src/main/java/org/thingsboard/server/service/qr/MobileSecretCaffeineCache.java

@ -24,10 +24,10 @@ import org.thingsboard.server.common.data.security.model.JwtPair;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("QRSecretCache")
public class QRSecretCaffeineCache extends CaffeineTbTransactionalCache<String, JwtPair> {
public class MobileSecretCaffeineCache extends CaffeineTbTransactionalCache<String, JwtPair> {
public QRSecretCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.QR_SECRET_KEY_CACHE);
public MobileSecretCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.MOBILE_SECRET_KEY_CACHE);
}
}

2
application/src/main/java/org/thingsboard/server/service/qr/QRSecretEvictEvent.java → application/src/main/java/org/thingsboard/server/service/qr/MobileSecretEvictEvent.java

@ -18,7 +18,7 @@ package org.thingsboard.server.service.qr;
import lombok.Data;
@Data
public class QRSecretEvictEvent {
public class MobileSecretEvictEvent {
private final String secret;

6
application/src/main/java/org/thingsboard/server/service/qr/QRSecretRedisCache.java → application/src/main/java/org/thingsboard/server/service/qr/MobileSecretRedisCache.java

@ -28,9 +28,9 @@ import org.thingsboard.server.common.data.security.model.JwtPair;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("QRSecretCache")
public class QRSecretRedisCache extends RedisTbTransactionalCache<UserId, JwtPair> {
public class MobileSecretRedisCache extends RedisTbTransactionalCache<UserId, JwtPair> {
public QRSecretRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.QR_SECRET_KEY_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(JwtPair.class));
public MobileSecretRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.MOBILE_SECRET_KEY_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(JwtPair.class));
}
}

4
application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java

@ -74,7 +74,7 @@ import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTING
@Slf4j
public class DefaultSystemSecurityService implements SystemSecurityService {
public static final int DEFAULT_QR_SECRET_KEY_LENGTH = 64;
public static final int DEFAULT_MOBILE_SECRET_KEY_LENGTH = 64;
@Autowired
private AdminSettingsService adminSettingsService;
@ -110,7 +110,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
securitySettings.setPasswordPolicy(new UserPasswordPolicy());
securitySettings.getPasswordPolicy().setMinimumLength(6);
securitySettings.getPasswordPolicy().setMaximumLength(72);
securitySettings.setQrSecretKeyLength(DEFAULT_QR_SECRET_KEY_LENGTH);
securitySettings.setMobileSecretKeyLength(DEFAULT_MOBILE_SECRET_KEY_LENGTH);
}
return securitySettings;
}

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

@ -582,9 +582,9 @@ cache:
mobileAppSettings:
timeToLiveInMinutes: "${CACHE_SPECS_MOBILE_APP_SETTINGS_TTL:1440}" # Mobile application cache TTL
maxSize: "${CACHE_SPECS_MOBILE_APP_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled
qrSecretKey:
timeToLiveInMinutes: "${CACHE_QR_SECRET_KEY_TTL:2}" # QR secret key cache TTL
maxSize: "${CACHE_QR_SECRET_KEY_MAX_SIZE:10000}" # 0 means the cache is disabled
mobileSecretKey:
timeToLiveInMinutes: "${CACHE_MOBILE_SECRET_KEY_TTL:2}" # QR secret key cache TTL
maxSize: "${CACHE_MOBILE_SECRET_KEY_MAX_SIZE:10000}" # 0 means the cache is disabled
# Deliberately placed outside the 'specs' group above
notificationRules:

24
application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java

@ -23,7 +23,6 @@ import org.junit.Test;
import org.mockito.Mockito;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.service.DaoSqlTest;
@ -182,27 +181,4 @@ public class AdminControllerTest extends AbstractControllerTest {
resetJwtSettingsToDefault();
}
@Test
public void testSaveMobileAppSettings() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/admin/mobileAppSettings", MobileAppSettings.class);
assertThat(mobileAppSettings.getSettings().get("qrSecretKeyRefreshRateInMin").asInt()).isEqualTo(1);
JsonNode jsonValue = mobileAppSettings.getSettings();
((ObjectNode) jsonValue).put("useDefault", false);
doPost("/api/admin/mobileAppSettings", mobileAppSettings)
.andExpect(status().isOk());
doGet("/api/admin/mobileAppSettings")
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.settings.useDefault", is(false)));
// check refresh rate should be less than secret ttl (5 min)
((ObjectNode) jsonValue).put("qrSecretKeyRefreshRateInMin", 6);
doPost("/api/admin/mobileAppSettings", mobileAppSettings)
.andExpect(status().isBadRequest());
}
}

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

@ -0,0 +1,132 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import 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.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();
QRCodeConfig qrCodeConfig = new QRCodeConfig();
qrCodeConfig.setQrCodeLabel(TEST_LABEL);
MobileAppSettings mobileAppSettings = new MobileAppSettings();
mobileAppSettings.setUseDefault(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);
}
@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.isUseDefault()).isTrue();
mobileAppSettings.setUseDefault(false);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
MobileAppSettings updatedMobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
assertThat(updatedMobileAppSettings.isUseDefault()).isFalse();
}
@Test
public void testGetApplicationAssociations() throws Exception {
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 secret = parsedDeepLink.group(2);
String ttl = parsedDeepLink.group(3);
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();
}
}

1
application/src/test/resources/application-test.properties

@ -8,7 +8,6 @@ transport.lwm2m.bootstrap.security.credentials.keystore.store_file=lwm2m/credent
transport.lwm2m.security.trust-credentials.enabled=true
transport.lwm2m.security.trust-credentials.type=KEYSTORE
transport.lwm2m.security.trust-credentials.keystore.store_file=lwm2m/credentials/lwm2mtruststorechain.jks
cache.specs.qrSecretKey.timeToLiveInMinutes=5
# Edge disabled to speed up the context init. Will be enabled by @TestPropertySource in respective tests
edges.enabled=false

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

@ -47,5 +47,5 @@ public class CacheConstants {
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_SECRET_KEY_CACHE = "qrSecretKey";
public static final String MOBILE_SECRET_KEY_CACHE = "mobileSecretKey";
}

38
common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java

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

23
common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java

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

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

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

36
common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java

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

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

@ -15,49 +15,24 @@
*/
package org.thingsboard.server.common.data.mobile;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.validation.Valid;
import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serializable;
import static org.thingsboard.server.common.data.BaseDataWithAdditionalInfo.getJson;
import static org.thingsboard.server.common.data.BaseDataWithAdditionalInfo.setJson;
@Data
public class MobileAppSettings implements Serializable {
private static final long serialVersionUID = 2628323657987010348L;
private TenantId tenantId;
boolean useDefault;
@Valid
private AndroidConfig androidConfig;
@Valid
private IosConfig iosConfig;
@Valid
private QRCodeConfig qrCodeConfig;
@NoXss
@Length(fieldName = "appPackage")
private String appPackage;
@NoXss
@Length(fieldName = "sha256CertFingerprints")
private String sha256CertFingerprints;
@NoXss
@Length(fieldName = "appId")
private String appId;
@NoXss
@Length(fieldName = "settings", max = 10000000)
private transient JsonNode settings;
@JsonIgnore
private byte[] settingsBytes;
public JsonNode getSettings() {
return getJson(() -> settings, () -> settingsBytes);
}
public void setSettings(JsonNode settings) {
setJson(settings, json -> this.settings = json, bytes -> this.settingsBytes = bytes);
}
}

38
common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java

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

4
common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java

@ -32,6 +32,6 @@ public class SecuritySettings implements Serializable {
private Integer maxFailedLoginAttempts;
@Schema(description = "Email to use for notifications about locked users." )
private String userLockoutNotificationEmail;
@Schema(description = "QR secret key length" )
private Integer qrSecretKeyLength;
@Schema(description = "Mobile secret key length" )
private Integer mobileSecretKeyLength;
}

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

@ -15,35 +15,27 @@
*/
package org.thingsboard.server.dao.mobile;
import com.fasterxml.jackson.databind.node.ObjectNode;
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.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.BadgePosition;
import org.thingsboard.server.common.data.mobile.BadgeStyle;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.mobile.QRCodeConfig;
import org.thingsboard.server.dao.entity.AbstractCachedService;
import org.thingsboard.server.dao.exception.DataValidationException;
@Service
@Slf4j
@RequiredArgsConstructor
public class BaseMobileAppSettingsService extends AbstractCachedService<TenantId, MobileAppSettings, MobileAppSettingsEvictEvent> implements MobileAppSettingsService {
@Value("${cache.specs.qrSecretKey.timeToLiveInMinutes:2}")
private int secretKeyTtl;
private static final int MIN_TIME_TO_DOWNLOAD_MOBILE_APP_IN_MIN = 1;
private static final String DEFAULT_QR_CODE_LABEL = "Scan to connect or download mobile app";
private final MobileAppSettingsDao mobileAppSettingsDao;
@Override
public MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings settings) {
if (settings.getSettings() != null && settings.getSettings().get("qrSecretKeyRefreshRateInMin") != null
&& (settings.getSettings().get("qrSecretKeyRefreshRateInMin").asInt() + MIN_TIME_TO_DOWNLOAD_MOBILE_APP_IN_MIN > secretKeyTtl)) {
throw new DataValidationException("Refresh rate should be less than server secret key ttl");
}
MobileAppSettings mobileAppSettings = mobileAppSettingsDao.save(tenantId, settings);
publishEvictEvent(new MobileAppSettingsEvictEvent(tenantId));
return mobileAppSettings;
@ -66,27 +58,18 @@ public class BaseMobileAppSettingsService extends AbstractCachedService<TenantId
private MobileAppSettings constructMobileAppSettings(MobileAppSettings mobileAppSettings) {
if (mobileAppSettings == null) {
mobileAppSettings = new MobileAppSettings();
mobileAppSettings.setUseDefault(true);
ObjectNode settings = JacksonUtil.newObjectNode();
settings.put("useDefault", true);
settings.put("showOnHomePage", true);
settings.put("qrLabel", "Scan to connect or download mobile app");
settings.put("qrSecretKeyRefreshRateInMin", 1);
ObjectNode androidSettings = JacksonUtil.newObjectNode();
androidSettings.put("badge", "original");
androidSettings.put("badgePosition", "Right");
settings.set("android", androidSettings);
ObjectNode iOSSettings = JacksonUtil.newObjectNode();
iOSSettings.put("badge", "original");
iOSSettings.put("badgePosition", "Right");
settings.set("iOS", iOSSettings);
mobileAppSettings.setSettings(settings);
QRCodeConfig qrCodeConfig = new QRCodeConfig();
qrCodeConfig.setQrCodeLabel(DEFAULT_QR_CODE_LABEL);
qrCodeConfig.setShowOnHomePage(true);
qrCodeConfig.setBadgeEnabled(true);
qrCodeConfig.setLabelEnabled(true);
qrCodeConfig.setBadgePosition(BadgePosition.RIGHT);
qrCodeConfig.setBadgeStyle(BadgeStyle.ORIGINAL);
mobileAppSettings.setQrCodeConfig(qrCodeConfig);
}
return mobileAppSettings;
}
}

2
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java

@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.mobile.MobileAppSettings;
public interface MobileAppSettingsDao {
MobileAppSettings save(TenantId tenantId, MobileAppSettings whiteLabeling);
MobileAppSettings save(TenantId tenantId, MobileAppSettings appSettings);
MobileAppSettings findByTenantId(TenantId tenantId);

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

@ -663,10 +663,10 @@ public class ModelConstants {
* Mobile application settings constants.
*/
public static final String MOBILE_APP_SETTINGS_TABLE_NAME = "mobile_app_settings";
public static final String MOBILE_APP_SETTINGS_APP_PACKAGE_PROPERTY = "app_package";
public static final String MOBILE_APP_SETTINGS_SHA256_CERT_FINGERPRINTS_PROPERTY = "sha256_cert_fingerprints";
public static final String MOBILE_APP_APP_ID_PROPERTY = "app_id";
public static final String MOBILE_APP_SETTINGS_PROPERTY = "settings";
public static final String MOBILE_APP_SETTINGS_USE_DEFAULT_PROPERTY = "use_default";
public static final String MOBILE_APP_SETTINGS_ANDROID_CONFIG_PROPERTY = "android_config";
public static final String MOBILE_APP_IOS_CONFIG_PROPERTY = "ios_config";
public static final String MOBILE_APP_QR_CODE_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};

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

@ -23,8 +23,12 @@ import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.common.util.JacksonUtil;
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.ModelConstants;
import org.thingsboard.server.dao.model.ToData;
import org.thingsboard.server.dao.util.mapping.JsonConverter;
@ -42,32 +46,32 @@ public class MobileAppSettingsEntity implements ToData<MobileAppSettings>, Seria
@Column(name = ModelConstants.TENANT_ID_COLUMN, columnDefinition = "uuid")
protected UUID tenantId;
@Column(name = ModelConstants.MOBILE_APP_SETTINGS_APP_PACKAGE_PROPERTY)
private String appPackage;
@Column(name = ModelConstants.MOBILE_APP_SETTINGS_USE_DEFAULT_PROPERTY)
private boolean useDefault;
@Column(name = ModelConstants.MOBILE_APP_SETTINGS_SHA256_CERT_FINGERPRINTS_PROPERTY)
private String sha256CertFingerprints;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.MOBILE_APP_SETTINGS_ANDROID_CONFIG_PROPERTY)
private JsonNode androidConfig;
@Column(name = ModelConstants.MOBILE_APP_APP_ID_PROPERTY)
private String appId;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.MOBILE_APP_IOS_CONFIG_PROPERTY)
private JsonNode iosConfig;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.MOBILE_APP_SETTINGS_PROPERTY)
private JsonNode settings;
@Column(name = ModelConstants.MOBILE_APP_QR_CODE_CONFIG_PROPERTY)
private JsonNode qrCodeConfig;
public MobileAppSettingsEntity(MobileAppSettings mobileAppSettings) {
this.tenantId = mobileAppSettings.getTenantId().getId();
if (mobileAppSettings.getAppPackage() != null) {
this.appPackage = mobileAppSettings.getAppPackage();
this.useDefault = mobileAppSettings.isUseDefault();
if (mobileAppSettings.getAndroidConfig() != null) {
this.androidConfig = JacksonUtil.valueToTree(mobileAppSettings.getAndroidConfig());
}
if (mobileAppSettings.getSha256CertFingerprints() != null) {
this.sha256CertFingerprints = mobileAppSettings.getSha256CertFingerprints();
if (mobileAppSettings.getIosConfig() != null) {
this.iosConfig = JacksonUtil.valueToTree(mobileAppSettings.getIosConfig());
}
if (mobileAppSettings.getAppId() != null) {
this.appId = mobileAppSettings.getAppId();
}
if (mobileAppSettings.getSettings() != null) {
this.settings = mobileAppSettings.getSettings();
if (mobileAppSettings.getQrCodeConfig() != null) {
this.qrCodeConfig = JacksonUtil.valueToTree(mobileAppSettings.getQrCodeConfig());
}
}
@ -75,17 +79,15 @@ public class MobileAppSettingsEntity implements ToData<MobileAppSettings>, Seria
public MobileAppSettings toData() {
MobileAppSettings mobileAppSettings = new MobileAppSettings();
mobileAppSettings.setTenantId(TenantId.fromUUID(tenantId));
if (appPackage != null) {
mobileAppSettings.setAppPackage(appPackage);
}
if (sha256CertFingerprints != null) {
mobileAppSettings.setSha256CertFingerprints(sha256CertFingerprints);
mobileAppSettings.setUseDefault(useDefault);
if (qrCodeConfig != null) {
mobileAppSettings.setAndroidConfig(JacksonUtil.convertValue(androidConfig, AndroidConfig.class));
}
if (appId != null) {
mobileAppSettings.setAppId(appId);
if (qrCodeConfig != null) {
mobileAppSettings.setIosConfig(JacksonUtil.convertValue(iosConfig, IosConfig.class));
}
if (settings != null) {
mobileAppSettings.setSettings(settings);
if (qrCodeConfig != null) {
mobileAppSettings.setQrCodeConfig(JacksonUtil.convertValue(qrCodeConfig, QRCodeConfig.class));
}
return mobileAppSettings;
}

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

@ -36,8 +36,8 @@ public class JpaMobileAppDao extends JpaAbstractDaoListeningExecutorService impl
private MobileAppSettingsRepository mobileAppSettingsRepository;
@Override
public MobileAppSettings save(TenantId tenantId, MobileAppSettings whiteLabeling) {
return DaoUtil.getData(mobileAppSettingsRepository.save(new MobileAppSettingsEntity(whiteLabeling)));
public MobileAppSettings save(TenantId tenantId, MobileAppSettings mobileAppSettings) {
return DaoUtil.getData(mobileAppSettingsRepository.save(new MobileAppSettingsEntity(mobileAppSettings)));
}
@Override

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

@ -897,8 +897,8 @@ CREATE TABLE IF NOT EXISTS queue_stats (
CREATE TABLE IF NOT EXISTS mobile_app_settings (
tenant_id UUID NOT NULL,
app_package VARCHAR(100) UNIQUE,
sha256_cert_fingerprints VARCHAR(10000),
app_id VARCHAR(100) UNIQUE,
settings VARCHAR(100000)
use_default boolean,
android_config VARCHAR(1000),
ios_config VARCHAR(1000),
qr_code_config VARCHAR(100000)
);
Loading…
Cancel
Save