Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[QR-code] - Widget redirects to Mobile app #10591

Merged
merged 25 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d0ff4a2
mobile app QR code initial implementation
dashevchenko Apr 18, 2024
67ba644
moved all apis to one controller
dashevchenko Apr 24, 2024
b216898
updated default mobile app settings, fixed permission
dashevchenko Apr 24, 2024
dc5f592
added mobile app settings cleanup for tenant deletion
dashevchenko Apr 25, 2024
0003caf
added id, createdTime to mobile app setting entity, added MobileAppSe…
dashevchenko Apr 30, 2024
3e0fad4
merged with master
dashevchenko Apr 30, 2024
f934863
fixed api test
dashevchenko May 2, 2024
949eb67
merged with master
dashevchenko May 2, 2024
0bf5290
code formatting
dashevchenko May 2, 2024
a057f42
UI: mobile app QR code UI initial implementation
rusikv May 2, 2024
4ee969f
fixed mobile application validator
dashevchenko May 5, 2024
953c215
UI: added separate mobile qrcode widget to cards bundle
rusikv May 7, 2024
012457e
Add mobileQrEnabled to system params
ViacheslavKlimov May 7, 2024
0d7d9b4
Refactoring for mobile settings api
ViacheslavKlimov May 7, 2024
d163b56
UI: mobile qrcode improvements
rusikv May 7, 2024
034ed62
Merge branch 'qrCode' of github.com:dashevchenko/thingsboard into qrCode
vvlladd28 May 7, 2024
2bbeb6a
UI: Added support mobileQrEnabled settings in tenant admin home page …
vvlladd28 May 7, 2024
0bc0422
UI: added mobile app qr code to sys admin home page
rusikv May 9, 2024
14ebfe7
UI: mobile app qrcode widgets update
rusikv May 9, 2024
2187246
UI: added preview images for mobile app qr code widgets
rusikv May 10, 2024
bb8be27
Merge remote-tracking branch 'upstream/master' into qrCode
rusikv May 10, 2024
ffbb2c7
UI: refactor mobile app qr code widgets validationm, cleanup widgets …
rusikv May 10, 2024
9d71bb7
UI: clean up sysadmin, tenant home pages jsons
rusikv May 10, 2024
45ed484
UI: mobile app qr code improvements, cleanup
rusikv May 15, 2024
a4e5b94
Update home-links-routing.module.ts
ikulikov May 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions application/src/main/data/upgrade/3.6.4/schema_update.sql
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,15 @@ DELETE FROM asset WHERE type='TbServiceQueue';
DELETE FROM asset_profile WHERE name ='TbServiceQueue';

-- QUEUE STATS UPDATE END

-- MOBILE APPS TABLE CREATE START

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

-- MOBILE APPS TABLE CREATE END
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
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;
Expand All @@ -75,6 +76,7 @@
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;
Expand All @@ -95,6 +97,7 @@

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
Expand All @@ -119,6 +122,7 @@ 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)
Expand Down Expand Up @@ -482,4 +486,31 @@ public void codeProcessingUrl(
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)
dashevchenko marked this conversation as resolved.
Show resolved Hide resolved
dashevchenko marked this conversation as resolved.
Show resolved Hide resolved
@ResponseStatus(value = HttpStatus.OK)
dashevchenko marked this conversation as resolved.
Show resolved Hide resolved
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)
dashevchenko marked this conversation as resolved.
Show resolved Hide resolved
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/mobileAppSettings", method = RequestMethod.GET)
dashevchenko marked this conversation as resolved.
Show resolved Hide resolved
@ResponseBody
dashevchenko marked this conversation as resolved.
Show resolved Hide resolved
public MobileAppSettings getMobileAppSettings() throws ThingsboardException {
SecurityUser currentUser = getCurrentUser();
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ);

return mobileAppSettingsService.getMobileAppSettings(currentUser.getTenantId());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* 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 {
dashevchenko marked this conversation as resolved.
Show resolved Hide resolved

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

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

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
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 QRSecretCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.QR_SECRET_KEY_CACHE);
}

}
Original file line number Diff line number Diff line change
@@ -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.service.qr;

import lombok.Data;

@Data
public class QRSecretEvictEvent {

private final String secret;

}
Original file line number Diff line number Diff line change
@@ -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.service.qr;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
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.UserId;
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 QRSecretRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.QR_SECRET_KEY_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(JwtPair.class));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.qr;

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 {

String generateSecret(SecurityUser securityUser);

JwtPair getJwtPair(String secret) throws ThingsboardException;

}