From f44c05f285ca463a7d0bd2b32b1261bd96424586 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 23 Jan 2024 16:33:48 +0200 Subject: [PATCH] Support for multiple mobile sessions --- .../main/data/upgrade/3.6.2/schema_update.sql | 2 - .../server/controller/UserController.java | 30 ++++++++----- .../MobileAppNotificationChannel.java | 41 ++++++++++-------- .../server/controller/AbstractWebTest.java | 4 ++ .../notification/NotificationApiTest.java | 31 +++++++++----- .../server/dao/user/UserService.java | 11 +++-- .../common/data/mobile/MobileSessionInfo.java | 23 ++++++++++ .../data/{ => mobile}/UserMobileInfo.java | 9 ++-- .../server/dao/sql/user/JpaUserDao.java | 6 --- .../dao/sql/user/JpaUserSettingsDao.java | 8 ++++ .../server/dao/sql/user/UserRepository.java | 8 ---- .../dao/sql/user/UserSettingsRepository.java | 7 ++++ .../thingsboard/server/dao/user/UserDao.java | 2 - .../server/dao/user/UserServiceImpl.java | 42 ++++++++++++++----- .../server/dao/user/UserSettingsDao.java | 5 +++ .../resources/sql/schema-entities-idx.sql | 2 - 16 files changed, 158 insertions(+), 73 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileSessionInfo.java rename common/data/src/main/java/org/thingsboard/server/common/data/{ => mobile}/UserMobileInfo.java (83%) diff --git a/application/src/main/data/upgrade/3.6.2/schema_update.sql b/application/src/main/data/upgrade/3.6.2/schema_update.sql index 9fb7921d0b..00d334b17d 100644 --- a/application/src/main/data/upgrade/3.6.2/schema_update.sql +++ b/application/src/main/data/upgrade/3.6.2/schema_update.sql @@ -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'; diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index 0269f6ec6f..a08e5e4bf7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -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 { diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java index 0d9f8044cf..a1de832d14 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java @@ -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 { private final FirebaseService firebaseService; @@ -43,24 +45,29 @@ public class MobileAppNotificationChannel implements NotificationChannel 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 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"); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 065e75d3e8..63c974fa96 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -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) { diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index be43e8dc08..d5a089dc82 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -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); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index 764f59d48b..f137bcc334 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -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 findMobileSessions(TenantId tenantId, UserId userId); + + MobileSessionInfo findMobileSession(TenantId tenantId, UserId userId, String mobileToken); + + void removeMobileSession(TenantId tenantId, String mobileToken); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileSessionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileSessionInfo.java new file mode 100644 index 0000000000..721274f33a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileSessionInfo.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; + +import lombok.Data; + +@Data +public class MobileSessionInfo { + private long fcmTokenTimestamp; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/UserMobileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java similarity index 83% rename from common/data/src/main/java/org/thingsboard/server/common/data/UserMobileInfo.java rename to common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java index 90680e1fff..21f72dd4db 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/UserMobileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java @@ -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 sessions; + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java index 8352df0323..7749dbff29 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java @@ -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 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()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java index 1feabaacb5..52a78c41fe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java @@ -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 findByTypeAndPath(TenantId tenantId, UserSettingsType type, String... path) { + return DaoUtil.convertDataList(userSettingsRepository.findByTypeAndPathExisting(type.name(), path)); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java index 3419cd6340..79b0a00308 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java @@ -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 { 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); - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java index cb81675a80..7f50b5ad5b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java @@ -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 { + @Query(value = "SELECT * FROM user_settings WHERE type = :type AND (settings #> :path) IS NOT NULL", nativeQuery = true) + List findByTypeAndPathExisting(@Param("type") String type, @Param("path") String[] path); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java index 59525174e0..42add1bbe2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java @@ -101,6 +101,4 @@ public interface UserDao extends Dao, TenantEntityDao { PageData findByAuthorityAndTenantProfilesIds(Authority authority, List tenantProfilesIds, PageLink pageLink); - void unassignFcmToken(TenantId tenantId, String fcmToken); - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 01b10863f9..4f662daba3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -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 userValidator; private final DataValidator 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 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 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 diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java index f2e5474c58..23734118a9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java @@ -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 findByTypeAndPath(TenantId tenantId, UserSettingsType type, String... path); + } diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 29adde3c8c..a177c1325f 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -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';