Support for multiple mobile sessions

This commit is contained in:
ViacheslavKlimov 2024-01-23 16:33:48 +02:00
parent e0d37c31dc
commit f44c05f285
16 changed files with 158 additions and 73 deletions

View File

@ -33,5 +33,3 @@ $$
END IF;
END;
$$;
CREATE INDEX IF NOT EXISTS idx_user_settings_mobile_fcm_token ON user_settings ((settings ->> 'fcmToken')) WHERE type = 'MOBILE';

View File

@ -26,11 +26,13 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@ -42,7 +44,6 @@ import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserEmailInfo;
import org.thingsboard.server.common.data.UserMobileInfo;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -51,6 +52,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.mobile.MobileSessionInfo;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
@ -83,7 +85,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION;
@ -120,6 +121,7 @@ public class UserController extends BaseController {
public static final String PATHS = "paths";
public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!";
public static final String ACTIVATE_URL_PATTERN = "%s/api/noauth/activate?activateToken=%s";
public static final String MOBILE_TOKEN_HEADER = "X-Mobile-Token";
@Value("${security.user_token_access_enabled}")
private boolean userTokenAccessEnabled;
@ -588,17 +590,25 @@ public class UserController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping("/user/mobile/info")
public UserMobileInfo getMobileInfo(@AuthenticationPrincipal SecurityUser securityUser) {
return Optional.ofNullable(userService.findMobileInfo(securityUser.getTenantId(), securityUser.getId()))
.orElseGet(UserMobileInfo::new);
@GetMapping("/user/mobile/session")
public MobileSessionInfo getMobileSession(@RequestHeader(MOBILE_TOKEN_HEADER) String mobileToken,
@AuthenticationPrincipal SecurityUser user) {
return userService.findMobileSession(user.getTenantId(), user.getId(), mobileToken);
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping("/user/mobile/info")
public void saveMobileInfo(@RequestBody UserMobileInfo mobileInfo,
@AuthenticationPrincipal SecurityUser securityUser) {
userService.saveMobileInfo(securityUser.getTenantId(), securityUser.getId(), mobileInfo);
@PostMapping("/user/mobile/session")
public void saveMobileSession(@RequestBody MobileSessionInfo sessionInfo,
@RequestHeader(MOBILE_TOKEN_HEADER) String mobileToken,
@AuthenticationPrincipal SecurityUser user) {
userService.saveMobileSession(user.getTenantId(), user.getId(), mobileToken, sessionInfo);
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@DeleteMapping("/user/mobile/session")
public void removeMobileSession(@RequestHeader(MOBILE_TOKEN_HEADER) String mobileToken,
@AuthenticationPrincipal SecurityUser user) {
userService.removeMobileSession(user.getTenantId(), mobileToken);
}
private void checkNotReserved(String strType, UserSettingsType type) throws ThingsboardException {

View File

@ -18,10 +18,10 @@ package org.thingsboard.server.service.notification.channels;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.MessagingErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.notification.FirebaseService;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserMobileInfo;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig;
@ -31,10 +31,12 @@ import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
import java.util.Optional;
import java.util.HashSet;
import java.util.Set;
@Component
@RequiredArgsConstructor
@Slf4j
public class MobileAppNotificationChannel implements NotificationChannel<User, MobileAppDeliveryMethodNotificationTemplate> {
private final FirebaseService firebaseService;
@ -43,24 +45,29 @@ public class MobileAppNotificationChannel implements NotificationChannel<User, M
@Override
public void sendNotification(User recipient, MobileAppDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
UserMobileInfo mobileInfo = userService.findMobileInfo(recipient.getTenantId(), recipient.getId());
String fcmToken = Optional.ofNullable(mobileInfo)
.map(UserMobileInfo::getFcmToken)
.orElseThrow(() -> new IllegalArgumentException("User doesn't use the mobile app"));
var mobileSessions = userService.findMobileSessions(recipient.getTenantId(), recipient.getId());
if (mobileSessions.isEmpty()) {
throw new IllegalArgumentException("User doesn't use the mobile app");
}
MobileAppNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(NotificationDeliveryMethod.MOBILE_APP);
try {
firebaseService.sendMessage(ctx.getTenantId(), config.getFirebaseServiceAccountCredentials(),
fcmToken, processedTemplate.getSubject(), processedTemplate.getBody());
} catch (FirebaseMessagingException e) {
MessagingErrorCode errorCode = e.getMessagingErrorCode();
if (errorCode == MessagingErrorCode.UNREGISTERED || errorCode == MessagingErrorCode.INVALID_ARGUMENT) {
// the token is no longer valid
mobileInfo.setFcmToken(null);
userService.saveMobileInfo(recipient.getTenantId(), recipient.getId(), mobileInfo);
throw new IllegalArgumentException("User doesn't use the mobile app");
String credentials = config.getFirebaseServiceAccountCredentials();
Set<String> validTokens = new HashSet<>(mobileSessions.keySet());
for (String token : mobileSessions.keySet()) {
try {
firebaseService.sendMessage(ctx.getTenantId(), credentials, token, processedTemplate.getSubject(), processedTemplate.getBody());
} catch (FirebaseMessagingException e) {
MessagingErrorCode errorCode = e.getMessagingErrorCode();
if (errorCode == MessagingErrorCode.UNREGISTERED || errorCode == MessagingErrorCode.INVALID_ARGUMENT) {
validTokens.remove(token);
userService.removeMobileSession(recipient.getTenantId(), token);
continue;
}
throw new RuntimeException("Failed to send message via FCM: " + e.getMessage(), e);
}
throw new RuntimeException("Failed to send message via FCM: " + e.getMessage(), e);
}
if (validTokens.isEmpty()) {
throw new IllegalArgumentException("User doesn't use the mobile app");
}
}

View File

@ -189,6 +189,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected String token;
protected String refreshToken;
protected String mobileToken;
protected String username;
protected TenantId tenantId;
@ -573,6 +574,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
if (this.token != null) {
request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token);
}
if (this.mobileToken != null) {
request.header(UserController.MOBILE_TOKEN_HEADER, this.mobileToken);
}
}
protected DeviceProfile createDeviceProfile(String name) {

View File

@ -31,13 +31,13 @@ import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.notification.FirebaseService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserMobileInfo;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileSessionInfo;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
@ -93,6 +93,7 @@ import static org.assertj.core.api.InstanceOfAssertFactories.type;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
@ -109,8 +110,6 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
@Autowired
private NotificationDao notificationDao;
@Autowired
private DbCallbackExecutorService executor;
@Autowired
private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel;
@MockBean
private FirebaseService firebaseService;
@ -726,14 +725,14 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
saveNotificationSettings(config);
loginCustomerUser();
UserMobileInfo customerMobileInfo = new UserMobileInfo();
customerMobileInfo.setFcmToken("customerFcmToken");
doPost("/api/user/mobile/info", customerMobileInfo).andExpect(status().isOk());
mobileToken = "customerFcmToken";
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
loginTenantAdmin();
UserMobileInfo tenantMobileInfo = new UserMobileInfo();
tenantMobileInfo.setFcmToken("tenantFcmToken");
doPost("/api/user/mobile/info", tenantMobileInfo).andExpect(status().isOk());
mobileToken = "tenantFcmToken1";
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
mobileToken = "tenantFcmToken2";
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
loginDifferentCustomer(); // with no mobile info
@ -748,7 +747,19 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
.contains("doesn't use the mobile app");
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("tenantFcmToken"), eq("Title"), eq("Message"));
eq("tenantFcmToken1"), eq("Title"), eq("Message"));
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("tenantFcmToken2"), eq("Title"), eq("Message"));
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("customerFcmToken"), eq("Title"), eq("Message"));
verifyNoMoreInteractions(firebaseService);
clearInvocations(firebaseService);
doDelete("/api/user/mobile/session").andExpect(status().isOk());
request = submitNotificationRequest(List.of(target.getId()), template.getId(), 0);
awaitNotificationRequest(request.getId());
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("tenantFcmToken1"), eq("Title"), eq("Message"));
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("customerFcmToken"), eq("Title"), eq("Message"));
verifyNoMoreInteractions(firebaseService);

View File

@ -17,7 +17,7 @@ package org.thingsboard.server.dao.user;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserMobileInfo;
import org.thingsboard.server.common.data.mobile.MobileSessionInfo;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.List;
import java.util.Map;
public interface UserService extends EntityDaoService {
@ -90,8 +91,12 @@ public interface UserService extends EntityDaoService {
void setLastLoginTs(TenantId tenantId, UserId userId);
void saveMobileInfo(TenantId tenantId, UserId userId, UserMobileInfo mobileInfo);
void saveMobileSession(TenantId tenantId, UserId userId, String mobileToken, MobileSessionInfo sessionInfo);
UserMobileInfo findMobileInfo(TenantId tenantId, UserId userId);
Map<String, MobileSessionInfo> findMobileSessions(TenantId tenantId, UserId userId);
MobileSessionInfo findMobileSession(TenantId tenantId, UserId userId, String mobileToken);
void removeMobileSession(TenantId tenantId, String mobileToken);
}

View File

@ -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;
import lombok.Data;
@Data
public class MobileSessionInfo {
private long fcmTokenTimestamp;
}

View File

@ -13,12 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data;
package org.thingsboard.server.common.data.mobile;
import lombok.Data;
import java.util.Map;
@Data
public class UserMobileInfo {
private String fcmToken;
private long fcmTokenTimestamp;
private Map<String, MobileSessionInfo> sessions;
}

View File

@ -33,7 +33,6 @@ import org.thingsboard.server.dao.user.UserDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
@ -135,11 +134,6 @@ public class JpaUserDao extends JpaAbstractDao<UserEntity, User> implements User
DaoUtil.toPageable(pageLink)));
}
@Override
public void unassignFcmToken(TenantId tenantId, String fcmToken) {
userRepository.unassignFcmToken(fcmToken);
}
@Override
public Long countByTenantId(TenantId tenantId) {
return userRepository.countByTenantId(tenantId.getId());

View File

@ -21,12 +21,15 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.UserSettingsEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService;
import org.thingsboard.server.dao.user.UserSettingsDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
@Slf4j
@Component
@SqlDao
@ -50,4 +53,9 @@ public class JpaUserSettingsDao extends JpaAbstractDaoListeningExecutorService i
userSettingsRepository.deleteById(id);
}
@Override
public List<UserSettings> findByTypeAndPath(TenantId tenantId, UserSettingsType type, String... path) {
return DaoUtil.convertDataList(userSettingsRepository.findByTypeAndPathExisting(type.name(), path));
}
}

View File

@ -18,10 +18,8 @@ package org.thingsboard.server.dao.sql.user;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.model.sql.UserEntity;
@ -73,10 +71,4 @@ public interface UserRepository extends JpaRepository<UserEntity, UUID> {
Long countByTenantId(UUID tenantId);
@Query(value = "UPDATE user_settings SET settings = settings - 'fcmToken' " +
"WHERE type = 'MOBILE' AND (settings ->> 'fcmToken') = :fcmToken", nativeQuery = true)
@Modifying
@Transactional
void unassignFcmToken(@Param("fcmToken") String fcmToken);
}

View File

@ -16,9 +16,16 @@
package org.thingsboard.server.dao.sql.user;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey;
import org.thingsboard.server.dao.model.sql.UserSettingsEntity;
import java.util.List;
public interface UserSettingsRepository extends JpaRepository<UserSettingsEntity, UserSettingsCompositeKey> {
@Query(value = "SELECT * FROM user_settings WHERE type = :type AND (settings #> :path) IS NOT NULL", nativeQuery = true)
List<UserSettingsEntity> findByTypeAndPathExisting(@Param("type") String type, @Param("path") String[] path);
}

View File

@ -101,6 +101,4 @@ public interface UserDao extends Dao<User>, TenantEntityDao {
PageData<User> findByAuthorityAndTenantProfilesIds(Authority authority, List<TenantProfileId> tenantProfilesIds, PageLink pageLink);
void unassignFcmToken(TenantId tenantId, String fcmToken);
}

View File

@ -21,7 +21,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.authentication.DisabledException;
@ -31,7 +30,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserMobileInfo;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
@ -40,6 +38,8 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.id.UserCredentialsId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.mobile.MobileSessionInfo;
import org.thingsboard.server.common.data.mobile.UserMobileInfo;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
@ -56,6 +56,7 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -90,6 +91,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
private final UserCredentialsDao userCredentialsDao;
private final UserAuthSettingsDao userAuthSettingsDao;
private final UserSettingsService userSettingsService;
private final UserSettingsDao userSettingsDao;
private final DataValidator<User> userValidator;
private final DataValidator<UserCredentials> userCredentialsValidator;
private final ApplicationEventPublisher eventPublisher;
@ -397,19 +399,39 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
}
@Override
public void saveMobileInfo(TenantId tenantId, UserId userId, UserMobileInfo mobileInfo) {
if (StringUtils.isNotEmpty(mobileInfo.getFcmToken())) {
// unassigning fcm token from other users, in case we didn't clean up it on log out or mobile app uninstall
userDao.unassignFcmToken(tenantId, mobileInfo.getFcmToken());
}
public void saveMobileSession(TenantId tenantId, UserId userId, String mobileToken, MobileSessionInfo sessionInfo) {
removeMobileSession(tenantId, mobileToken); // unassigning fcm token from other users, in case we didn't clean up it on log out or mobile app uninstall
UserMobileInfo mobileInfo = findMobileInfo(tenantId, userId).orElseGet(() -> {
UserMobileInfo newMobileInfo = new UserMobileInfo();
newMobileInfo.setSessions(new HashMap<>());
return newMobileInfo;
});
mobileInfo.getSessions().put(mobileToken, sessionInfo);
userSettingsService.updateUserSettings(tenantId, userId, UserSettingsType.MOBILE, JacksonUtil.valueToTree(mobileInfo));
}
@Override
public UserMobileInfo findMobileInfo(TenantId tenantId, UserId userId) {
public Map<String, MobileSessionInfo> findMobileSessions(TenantId tenantId, UserId userId) {
return findMobileInfo(tenantId, userId).map(UserMobileInfo::getSessions).orElse(Collections.emptyMap());
}
@Override
public MobileSessionInfo findMobileSession(TenantId tenantId, UserId userId, String mobileToken) {
return findMobileInfo(tenantId, userId).map(mobileInfo -> mobileInfo.getSessions().get(mobileToken)).orElse(null);
}
@Override
public void removeMobileSession(TenantId tenantId, String mobileToken) {
for (UserSettings userSettings : userSettingsDao.findByTypeAndPath(tenantId, UserSettingsType.MOBILE, "sessions", mobileToken)) {
((ObjectNode) userSettings.getSettings().get("sessions")).remove(mobileToken);
userSettingsService.saveUserSettings(tenantId, userSettings);
}
}
private Optional<UserMobileInfo> findMobileInfo(TenantId tenantId, UserId userId) {
return Optional.ofNullable(userSettingsService.findUserSettings(tenantId, userId, UserSettingsType.MOBILE))
.map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileInfo.class))
.orElse(null);
.map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileInfo.class));
}
@Override

View File

@ -18,6 +18,9 @@ package org.thingsboard.server.dao.user;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import java.util.List;
public interface UserSettingsDao {
@ -27,4 +30,6 @@ public interface UserSettingsDao {
void removeById(TenantId tenantId, UserSettingsCompositeKey key);
List<UserSettings> findByTypeAndPath(TenantId tenantId, UserSettingsType type, String... path);
}

View File

@ -127,5 +127,3 @@ CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag);
CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag);
CREATE INDEX IF NOT EXISTS idx_resource_type_public_resource_key ON resource(resource_type, public_resource_key);
CREATE INDEX IF NOT EXISTS idx_user_settings_mobile_fcm_token ON user_settings ((settings ->> 'fcmToken')) WHERE type = 'MOBILE';