Major refactoring for mobile app notifications

This commit is contained in:
ViacheslavKlimov 2023-12-28 12:30:41 +02:00
parent b256f3a8cd
commit 7bf58e3891
27 changed files with 154 additions and 57 deletions

View File

@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -41,6 +42,7 @@ import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserEmailInfo; 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.alarm.Alarm;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -81,6 +83,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD; import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION;
@ -584,6 +587,20 @@ public class UserController extends BaseController {
return userSettingsService.reportUserDashboardAction(currentUser.getTenantId(), currentUser.getId(), dashboardId, action); return userSettingsService.reportUserDashboardAction(currentUser.getTenantId(), currentUser.getId(), dashboardId, action);
} }
@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);
}
@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);
}
private void checkNotReserved(String strType, UserSettingsType type) throws ThingsboardException { private void checkNotReserved(String strType, UserSettingsType type) throws ThingsboardException {
if (type.isReserved()) { if (type.isReserved()) {
throw new ThingsboardException("Settings with type: " + strType + " are reserved for internal use!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); throw new ThingsboardException("Settings with type: " + strType + " are reserved for internal use!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);

View File

@ -15,54 +15,65 @@
*/ */
package org.thingsboard.server.service.notification.channels; package org.thingsboard.server.service.notification.channels;
import com.fasterxml.jackson.databind.JsonNode; import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.MessagingErrorCode;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.notification.FirebaseService; import org.thingsboard.rule.engine.api.notification.FirebaseService;
import org.thingsboard.server.common.data.User; 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.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.settings.MobileNotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.template.MobileDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate;
import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.notification.NotificationProcessingContext; import org.thingsboard.server.service.notification.NotificationProcessingContext;
import java.util.Optional; import java.util.Optional;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class MobileNotificationChannel implements NotificationChannel<User, MobileDeliveryMethodNotificationTemplate> { public class MobileAppNotificationChannel implements NotificationChannel<User, MobileAppDeliveryMethodNotificationTemplate> {
private final FirebaseService firebaseService; private final FirebaseService firebaseService;
private final UserService userService;
private final NotificationSettingsService notificationSettingsService; private final NotificationSettingsService notificationSettingsService;
@Override @Override
public void sendNotification(User recipient, MobileDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception { public void sendNotification(User recipient, MobileAppDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
String fcmToken = Optional.ofNullable(recipient.getAdditionalInfo()) UserMobileInfo mobileInfo = userService.findMobileInfo(recipient.getTenantId(), recipient.getId());
.map(info -> info.get("fcmToken")).filter(JsonNode::isTextual).map(JsonNode::asText) String fcmToken = Optional.ofNullable(mobileInfo)
.orElse(null); .map(UserMobileInfo::getFcmToken)
if (StringUtils.isEmpty(fcmToken)) { .orElseThrow(() -> new IllegalArgumentException("User doesn't use the mobile app"));
throw new RuntimeException("User doesn't have the mobile app installed");
}
MobileNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(NotificationDeliveryMethod.MOBILE); MobileAppNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(NotificationDeliveryMethod.MOBILE_APP);
try {
firebaseService.sendMessage(ctx.getTenantId(), config.getFirebaseServiceAccountCredentials(), firebaseService.sendMessage(ctx.getTenantId(), config.getFirebaseServiceAccountCredentials(),
fcmToken, processedTemplate.getSubject(), processedTemplate.getBody()); fcmToken, processedTemplate.getSubject(), processedTemplate.getBody());
} catch (FirebaseMessagingException e) {
if (e.getMessagingErrorCode() == MessagingErrorCode.UNREGISTERED) {
// 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");
}
throw new RuntimeException("Failed to send message via FCM: " + e.getMessage(), e);
}
} }
@Override @Override
public void check(TenantId tenantId) throws Exception { public void check(TenantId tenantId) throws Exception {
NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId); NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId);
if (!settings.getDeliveryMethodsConfigs().containsKey(NotificationDeliveryMethod.MOBILE)) { if (!settings.getDeliveryMethodsConfigs().containsKey(NotificationDeliveryMethod.MOBILE_APP)) {
throw new RuntimeException("Push-notifications to mobile are not configured"); throw new RuntimeException("Push-notifications to mobile are not configured");
} }
} }
@Override @Override
public NotificationDeliveryMethod getDeliveryMethod() { public NotificationDeliveryMethod getDeliveryMethod() {
return NotificationDeliveryMethod.MOBILE; return NotificationDeliveryMethod.MOBILE_APP;
} }
} }

View File

@ -48,7 +48,7 @@ public class DefaultFirebaseService implements FirebaseService {
.build(); .build();
@Override @Override
public void sendMessage(TenantId tenantId, String credentials, String fcmToken, String title, String body) { public void sendMessage(TenantId tenantId, String credentials, String fcmToken, String title, String body) throws FirebaseMessagingException {
FirebaseContext firebaseContext = contexts.asMap().compute(tenantId.toString(), (key, context) -> { FirebaseContext firebaseContext = contexts.asMap().compute(tenantId.toString(), (key, context) -> {
if (context == null) { if (context == null) {
return new FirebaseContext(key, credentials); return new FirebaseContext(key, credentials);
@ -65,11 +65,7 @@ public class DefaultFirebaseService implements FirebaseService {
.build()) .build())
.setToken(fcmToken) .setToken(fcmToken)
.build(); .build();
try {
firebaseContext.getMessaging().send(message); firebaseContext.getMessaging().send(message);
} catch (FirebaseMessagingException e) {
throw new RuntimeException("Failed to send message via FCM: " + e.getMessage(), e);
}
} }
public static class FirebaseContext { public static class FirebaseContext {

View File

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.user;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserMobileInfo;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.TenantProfileId;
@ -89,4 +90,8 @@ public interface UserService extends EntityDaoService {
void setLastLoginTs(TenantId tenantId, UserId userId); void setLastLoginTs(TenantId tenantId, UserId userId);
void saveMobileInfo(TenantId tenantId, UserId userId, UserMobileInfo mobileInfo);
UserMobileInfo findMobileInfo(TenantId tenantId, UserId userId);
} }

View File

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2023 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;
import lombok.Data;
@Data
public class UserMobileInfo {
private String fcmToken;
private long fcmTokenTimestamp;
}

View File

@ -26,9 +26,10 @@ public enum NotificationDeliveryMethod {
SMS("SMS"), SMS("SMS"),
SLACK("Slack"), SLACK("Slack"),
MICROSOFT_TEAMS("Microsoft Teams"), MICROSOFT_TEAMS("Microsoft Teams"),
MOBILE("mobile"); MOBILE_APP("mobile app");
@Getter @Getter
private final String name; private final String name;
// TODO: private final boolean allowUseSystemSettings;
} }

View File

@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
@Data @Data
public class MobileNotificationDeliveryMethodConfig implements NotificationDeliveryMethodConfig { public class MobileAppNotificationDeliveryMethodConfig implements NotificationDeliveryMethodConfig {
private String firebaseServiceAccountCredentialsFileName; private String firebaseServiceAccountCredentialsFileName;
@NotEmpty @NotEmpty
@ -29,7 +29,7 @@ public class MobileNotificationDeliveryMethodConfig implements NotificationDeliv
@Override @Override
public NotificationDeliveryMethod getMethod() { public NotificationDeliveryMethod getMethod() {
return NotificationDeliveryMethod.MOBILE; return NotificationDeliveryMethod.MOBILE_APP;
} }
} }

View File

@ -28,7 +28,7 @@ import java.io.Serializable;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method")
@JsonSubTypes({ @JsonSubTypes({
@Type(name = "SLACK", value = SlackNotificationDeliveryMethodConfig.class), @Type(name = "SLACK", value = SlackNotificationDeliveryMethodConfig.class),
@Type(name = "MOBILE", value = MobileNotificationDeliveryMethodConfig.class) @Type(name = "MOBILE_APP", value = MobileAppNotificationDeliveryMethodConfig.class)
}) })
public interface NotificationDeliveryMethodConfig extends Serializable { public interface NotificationDeliveryMethodConfig extends Serializable {

View File

@ -28,7 +28,8 @@ public class NotificationSettings implements Serializable {
@NotNull @NotNull
@Valid @Valid
// location on the screen, shown notifications count, timings of displaying
private Map<NotificationDeliveryMethod, NotificationDeliveryMethodConfig> deliveryMethodsConfigs; private Map<NotificationDeliveryMethod, NotificationDeliveryMethodConfig> deliveryMethodsConfigs;
// TODO: disable option, location on the screen, shown notifications count, timings of displaying
} }

View File

@ -25,7 +25,7 @@ import java.util.Set;
@RequiredArgsConstructor @RequiredArgsConstructor
public enum NotificationTargetType { public enum NotificationTargetType {
PLATFORM_USERS(Set.of(NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL, NotificationDeliveryMethod.SMS, NotificationDeliveryMethod.MOBILE)), PLATFORM_USERS(Set.of(NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL, NotificationDeliveryMethod.SMS, NotificationDeliveryMethod.MOBILE_APP)),
SLACK(Set.of(NotificationDeliveryMethod.SLACK)), SLACK(Set.of(NotificationDeliveryMethod.SLACK)),
MICROSOFT_TEAMS(Set.of(NotificationDeliveryMethod.MICROSOFT_TEAMS)); MICROSOFT_TEAMS(Set.of(NotificationDeliveryMethod.MICROSOFT_TEAMS));

View File

@ -35,7 +35,7 @@ import java.util.List;
@Type(name = "SMS", value = SmsDeliveryMethodNotificationTemplate.class), @Type(name = "SMS", value = SmsDeliveryMethodNotificationTemplate.class),
@Type(name = "SLACK", value = SlackDeliveryMethodNotificationTemplate.class), @Type(name = "SLACK", value = SlackDeliveryMethodNotificationTemplate.class),
@Type(name = "MICROSOFT_TEAMS", value = MicrosoftTeamsDeliveryMethodNotificationTemplate.class), @Type(name = "MICROSOFT_TEAMS", value = MicrosoftTeamsDeliveryMethodNotificationTemplate.class),
@Type(name = "MOBILE", value = MobileDeliveryMethodNotificationTemplate.class) @Type(name = "MOBILE_APP", value = MobileAppDeliveryMethodNotificationTemplate.class)
}) })
@Data @Data
@NoArgsConstructor @NoArgsConstructor

View File

@ -28,7 +28,7 @@ import java.util.List;
@NoArgsConstructor @NoArgsConstructor
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true) @ToString(callSuper = true)
public class MobileDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject { public class MobileAppDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject {
@NotEmpty @NotEmpty
private String subject; private String subject;
@ -38,19 +38,19 @@ public class MobileDeliveryMethodNotificationTemplate extends DeliveryMethodNoti
TemplatableValue.of(this::getSubject, this::setSubject) TemplatableValue.of(this::getSubject, this::setSubject)
); );
public MobileDeliveryMethodNotificationTemplate(MobileDeliveryMethodNotificationTemplate other) { public MobileAppDeliveryMethodNotificationTemplate(MobileAppDeliveryMethodNotificationTemplate other) {
super(other); super(other);
this.subject = other.subject; this.subject = other.subject;
} }
@Override @Override
public NotificationDeliveryMethod getMethod() { public NotificationDeliveryMethod getMethod() {
return NotificationDeliveryMethod.MOBILE; return NotificationDeliveryMethod.MOBILE_APP;
} }
@Override @Override
public MobileDeliveryMethodNotificationTemplate copy() { public MobileAppDeliveryMethodNotificationTemplate copy() {
return new MobileDeliveryMethodNotificationTemplate(this); return new MobileAppDeliveryMethodNotificationTemplate(this);
} }
@Override @Override

View File

@ -19,7 +19,14 @@ import lombok.Getter;
public enum UserSettingsType { public enum UserSettingsType {
GENERAL, VISITED_DASHBOARDS(true), QUICK_LINKS, DOC_LINKS, DASHBOARDS, GETTING_STARTED, NOTIFICATIONS; GENERAL,
VISITED_DASHBOARDS(true),
QUICK_LINKS,
DOC_LINKS,
DASHBOARDS,
GETTING_STARTED,
NOTIFICATIONS,
MOBILE(true);
@Getter @Getter
private final boolean reserved; private final boolean reserved;

View File

@ -135,6 +135,11 @@ public class JpaUserDao extends JpaAbstractDao<UserEntity, User> implements User
DaoUtil.toPageable(pageLink))); DaoUtil.toPageable(pageLink)));
} }
@Override
public void unassignFcmToken(TenantId tenantId, String fcmToken) {
userRepository.unassignFcmToken(fcmToken);
}
@Override @Override
public Long countByTenantId(TenantId tenantId) { public Long countByTenantId(TenantId tenantId) {
return userRepository.countByTenantId(tenantId.getId()); return userRepository.countByTenantId(tenantId.getId());

View File

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

View File

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

View File

@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.DisabledException;
@ -30,6 +31,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User; 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.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -43,6 +45,8 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.EntityCountService; import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent;
@ -85,6 +89,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
private final UserDao userDao; private final UserDao userDao;
private final UserCredentialsDao userCredentialsDao; private final UserCredentialsDao userCredentialsDao;
private final UserAuthSettingsDao userAuthSettingsDao; private final UserAuthSettingsDao userAuthSettingsDao;
private final UserSettingsService userSettingsService;
private final DataValidator<User> userValidator; private final DataValidator<User> userValidator;
private final DataValidator<UserCredentials> userCredentialsValidator; private final DataValidator<UserCredentials> userCredentialsValidator;
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
@ -391,6 +396,22 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
saveUser(tenantId, user); saveUser(tenantId, user);
} }
@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());
}
userSettingsService.updateUserSettings(tenantId, userId, UserSettingsType.MOBILE, JacksonUtil.valueToTree(mobileInfo));
}
@Override
public 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);
}
@Override @Override
public int increaseFailedLoginAttempts(TenantId tenantId, UserId userId) { public int increaseFailedLoginAttempts(TenantId tenantId, UserId userId) {
log.trace("Executing onUserLoginIncorrectCredentials [{}]", userId); log.trace("Executing onUserLoginIncorrectCredentials [{}]", userId);

View File

@ -19,6 +19,6 @@ import org.thingsboard.server.common.data.id.TenantId;
public interface FirebaseService { public interface FirebaseService {
void sendMessage(TenantId tenantId, String credentials, String fcmToken, String title, String body); void sendMessage(TenantId tenantId, String credentials, String fcmToken, String title, String body) throws Exception;
} }

View File

@ -75,14 +75,14 @@
<!-- <div tb-help="mobileSettings"></div>--> <!-- <div tb-help="mobileSettings"></div>-->
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<section formGroupName="MOBILE" style="margin-bottom: 16px;"> <section formGroupName="MOBILE_APP" style="margin-bottom: 16px;">
<tb-file-input formControlName="firebaseServiceAccountCredentials" <tb-file-input formControlName="firebaseServiceAccountCredentials"
dropLabel="{{ 'admin.select-firebase-service-account-file' | translate }}" dropLabel="{{ 'admin.select-firebase-service-account-file' | translate }}"
label="{{ 'admin.firebase-service-account-file' | translate }}" label="{{ 'admin.firebase-service-account-file' | translate }}"
accept=".json,application/json" accept=".json,application/json"
allowedExtensions="json" allowedExtensions="json"
[existingFileName]="notificationSettingsForm.get('deliveryMethodsConfigs.MOBILE.firebaseServiceAccountCredentialsFileName')?.value" [existingFileName]="notificationSettingsForm.get('deliveryMethodsConfigs.MOBILE_APP.firebaseServiceAccountCredentialsFileName')?.value"
(fileNameChanged)="notificationSettingsForm?.get('deliveryMethodsConfigs.MOBILE.firebaseServiceAccountCredentialsFileName').patchValue($event)"> (fileNameChanged)="notificationSettingsForm?.get('deliveryMethodsConfigs.MOBILE_APP.firebaseServiceAccountCredentialsFileName').patchValue($event)">
</tb-file-input> </tb-file-input>
</section> </section>
<div fxLayout="row" fxLayoutAlign="end center" fxLayout.xs="column" fxLayoutAlign.xs="end"> <div fxLayout="row" fxLayoutAlign="end center" fxLayout.xs="column" fxLayoutAlign.xs="end">

View File

@ -117,7 +117,7 @@ export class SmsProviderComponent extends PageComponent implements HasConfirmFor
SLACK: this.fb.group({ SLACK: this.fb.group({
botToken: [''] botToken: ['']
}), }),
MOBILE: this.fb.group({ MOBILE_APP: this.fb.group({
firebaseServiceAccountCredentialsFileName: [''], firebaseServiceAccountCredentialsFileName: [''],
firebaseServiceAccountCredentials: [''] firebaseServiceAccountCredentials: ['']
}) })

View File

@ -446,9 +446,9 @@
</form> </form>
</mat-step> </mat-step>
<mat-step *ngIf="!notificationRequestForm.get('useTemplate').value && <mat-step *ngIf="!notificationRequestForm.get('useTemplate').value &&
notificationRequestForm.get('template.configuration.deliveryMethodsTemplates.MOBILE.enabled').value" notificationRequestForm.get('template.configuration.deliveryMethodsTemplates.MOBILE_APP.enabled').value"
[stepControl]="mobileTemplateForm"> [stepControl]="mobileTemplateForm">
<ng-template matStepLabel>{{ 'notification.delivery-method.mobile' | translate }}</ng-template> <ng-template matStepLabel>{{ 'notification.delivery-method.mobile-app' | translate }}</ng-template>
<div class="tb-hint-available-params mat-body-2"> <div class="tb-hint-available-params mat-body-2">
<span class="content">{{ 'notification.input-field-support-templatization' | translate}}</span> <span class="content">{{ 'notification.input-field-support-templatization' | translate}}</span>
<span tb-help-popup="{{ notificationTemplateTypeTranslateMap.get(notificationType.GENERAL).helpId }}" <span tb-help-popup="{{ notificationTemplateTypeTranslateMap.get(notificationType.GENERAL).helpId }}"
@ -523,14 +523,14 @@
{{ preview.processedTemplates.SLACK.body }} {{ preview.processedTemplates.SLACK.body }}
</div> </div>
</section> </section>
<section class="preview-group notification" *ngIf="preview.processedTemplates.MOBILE?.enabled"> <section class="preview-group notification" *ngIf="preview.processedTemplates.MOBILE_APP?.enabled">
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center"> <div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<mat-icon class="tb-mat-18" svgIcon="mdi:cellphone-text"></mat-icon> <mat-icon class="tb-mat-18" svgIcon="mdi:cellphone-text"></mat-icon>
<div class="group-title" translate>notification.delivery-method.mobile-preview</div> <div class="group-title" translate>notification.delivery-method.mobile-app-preview</div>
</div> </div>
<div class="notification-content"> <div class="notification-content">
<div class="subject">{{ preview.processedTemplates.MOBILE.subject }}</div> <div class="subject">{{ preview.processedTemplates.MOBILE_APP.subject }}</div>
<div>{{ preview.processedTemplates.MOBILE.body }}</div> <div>{{ preview.processedTemplates.MOBILE_APP.body }}</div>
</div> </div>
</section> </section>
<section class="preview-group notification" *ngIf="preview.processedTemplates.MICROSOFT_TEAMS?.enabled"> <section class="preview-group notification" *ngIf="preview.processedTemplates.MICROSOFT_TEAMS?.enabled">

View File

@ -306,7 +306,7 @@ export class SentNotificationDialogComponent extends
allowConfigureDeliveryMethod(deliveryMethod: NotificationDeliveryMethod): boolean { allowConfigureDeliveryMethod(deliveryMethod: NotificationDeliveryMethod): boolean {
const tenantAllowConfigureDeliveryMethod = new Set([ const tenantAllowConfigureDeliveryMethod = new Set([
NotificationDeliveryMethod.SLACK, NotificationDeliveryMethod.SLACK,
NotificationDeliveryMethod.MOBILE NotificationDeliveryMethod.MOBILE_APP
]); ]);
if (deliveryMethod === NotificationDeliveryMethod.WEB) { if (deliveryMethod === NotificationDeliveryMethod.WEB) {
return false; return false;
@ -329,7 +329,7 @@ export class SentNotificationDialogComponent extends
return '/settings/outgoing-mail'; return '/settings/outgoing-mail';
case NotificationDeliveryMethod.SMS: case NotificationDeliveryMethod.SMS:
case NotificationDeliveryMethod.SLACK: case NotificationDeliveryMethod.SLACK:
case NotificationDeliveryMethod.MOBILE: case NotificationDeliveryMethod.MOBILE_APP:
return '/settings/notifications'; return '/settings/notifications';
} }
} }

View File

@ -148,7 +148,7 @@ export abstract class TemplateConfiguration<T, R = any> extends DialogComponent<
[NotificationDeliveryMethod.SMS, this.smsTemplateForm], [NotificationDeliveryMethod.SMS, this.smsTemplateForm],
[NotificationDeliveryMethod.SLACK, this.slackTemplateForm], [NotificationDeliveryMethod.SLACK, this.slackTemplateForm],
[NotificationDeliveryMethod.MICROSOFT_TEAMS, this.microsoftTeamsTemplateForm], [NotificationDeliveryMethod.MICROSOFT_TEAMS, this.microsoftTeamsTemplateForm],
[NotificationDeliveryMethod.MOBILE, this.mobileTemplateForm] [NotificationDeliveryMethod.MOBILE_APP, this.mobileTemplateForm]
]); ]);
} }

View File

@ -364,9 +364,9 @@
</div> </div>
</form> </form>
</mat-step> </mat-step>
<mat-step *ngIf="templateNotificationForm.get('configuration.deliveryMethodsTemplates.MOBILE.enabled').value" <mat-step *ngIf="templateNotificationForm.get('configuration.deliveryMethodsTemplates.MOBILE_APP.enabled').value"
[stepControl]="mobileTemplateForm"> [stepControl]="mobileTemplateForm">
<ng-template matStepLabel>{{ 'notification.delivery-method.mobile' | translate }}</ng-template> <ng-template matStepLabel>{{ 'notification.delivery-method.mobile-app' | translate }}</ng-template>
<div class="tb-hint-available-params mat-body-2"> <div class="tb-hint-available-params mat-body-2">
<span class="content">{{ 'notification.input-field-support-templatization' | translate}}</span> <span class="content">{{ 'notification.input-field-support-templatization' | translate}}</span>
<span tb-help-popup="{{ notificationTemplateTypeTranslateMap.get(templateNotificationForm.get('notificationType').value).helpId }}" <span tb-help-popup="{{ notificationTemplateTypeTranslateMap.get(templateNotificationForm.get('notificationType').value).helpId }}"

View File

@ -380,7 +380,7 @@ export enum NotificationDeliveryMethod {
EMAIL = 'EMAIL', EMAIL = 'EMAIL',
SLACK = 'SLACK', SLACK = 'SLACK',
MICROSOFT_TEAMS = 'MICROSOFT_TEAMS', MICROSOFT_TEAMS = 'MICROSOFT_TEAMS',
MOBILE = 'MOBILE' MOBILE_APP = 'MOBILE_APP'
} }
export const NotificationDeliveryMethodTranslateMap = new Map<NotificationDeliveryMethod, string>([ export const NotificationDeliveryMethodTranslateMap = new Map<NotificationDeliveryMethod, string>([
@ -389,7 +389,7 @@ export const NotificationDeliveryMethodTranslateMap = new Map<NotificationDelive
[NotificationDeliveryMethod.EMAIL, 'notification.delivery-method.email'], [NotificationDeliveryMethod.EMAIL, 'notification.delivery-method.email'],
[NotificationDeliveryMethod.SLACK, 'notification.delivery-method.slack'], [NotificationDeliveryMethod.SLACK, 'notification.delivery-method.slack'],
[NotificationDeliveryMethod.MICROSOFT_TEAMS, 'notification.delivery-method.microsoft-teams'], [NotificationDeliveryMethod.MICROSOFT_TEAMS, 'notification.delivery-method.microsoft-teams'],
[NotificationDeliveryMethod.MOBILE, 'notification.delivery-method.mobile'] [NotificationDeliveryMethod.MOBILE_APP, 'notification.delivery-method.mobile-app']
]); ]);
export enum NotificationRequestStatus { export enum NotificationRequestStatus {

View File

@ -3271,8 +3271,8 @@
"sms-preview": "SMS notification preview", "sms-preview": "SMS notification preview",
"web": "Web", "web": "Web",
"web-preview": "Web notification preview", "web-preview": "Web notification preview",
"mobile": "Mobile", "mobile-app": "Mobile app",
"mobile-preview": "Mobile notification preview" "mobile-app-preview": "Mobile app notification preview"
}, },
"delivery-method-not-configure-click": "Delivery method is not configured. Click to setup.", "delivery-method-not-configure-click": "Delivery method is not configured. Click to setup.",
"delivery-method-not-configure-contact": "Delivery method is not configured. Contact your system administrator.", "delivery-method-not-configure-contact": "Delivery method is not configured. Contact your system administrator.",