29 changed files with 552 additions and 364 deletions
@ -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(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue