diff --git a/application/src/main/data/upgrade/3.4.4/schema_update.sql b/application/src/main/data/upgrade/3.4.4/schema_update.sql index deaf8c6369..f8c133f352 100644 --- a/application/src/main/data/upgrade/3.4.4/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.4/schema_update.sql @@ -24,3 +24,9 @@ CREATE TABLE IF NOT EXISTS alarm_comment ( CONSTRAINT fk_alarm_comment_alarm_id FOREIGN KEY (alarm_id) REFERENCES alarm(id) ON DELETE CASCADE ) PARTITION BY RANGE (created_time); CREATE INDEX IF NOT EXISTS idx_alarm_comment_alarm_id ON alarm_comment(alarm_id); + +CREATE TABLE IF NOT EXISTS user_settings ( + user_id uuid NOT NULL CONSTRAINT user_settings_pkey PRIMARY KEY, + settings varchar(100000), + CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE +); diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 3f11fc59ac..970100d938 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -128,6 +128,7 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.user.UserSettingsService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; @@ -189,6 +190,9 @@ public abstract class BaseController { @Autowired protected UserService userService; + @Autowired + protected UserSettingsService userSettingsService; + @Autowired protected DeviceService deviceService; 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 c86851fb88..ed55bcf324 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -24,7 +25,10 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -32,6 +36,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @@ -43,6 +48,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.data.security.UserSettings; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.user.TbUserService; @@ -56,6 +62,8 @@ import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; + import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEFAULT_DASHBOARD; @@ -84,6 +92,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI public class UserController extends BaseController { public static final String USER_ID = "userId"; + public static final String JSON_PATHS = "jsonPaths"; 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"; @@ -378,4 +387,55 @@ public class UserController extends BaseController { } } + @ApiOperation(value = "Save user settings (saveUserSettings)", + notes = "Save user settings represented in json format for authorized user. " ) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PostMapping(value = "/user/settings") + public JsonNode saveUserSettings(@RequestBody JsonNode settings) throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + + UserSettings userSettings = new UserSettings(); + userSettings.setSettings(settings); + userSettings.setUserId(currentUser.getId()); + return userSettingsService.saveUserSettings(currentUser.getTenantId(), userSettings).getSettings(); + } + + @ApiOperation(value = "Update user settings (saveUserSettings)", + notes = "Update user settings for authorized user. Only specified json elements will be updated." + + "Example: you have such settings: {A:5, B:{C:10, D:5}}. Updating it with {A:10, E:6} will result in" + + "{A:10, B:{C:10, D:5}}, E:6") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PutMapping(value = "/user/settings") + public JsonNode putUserSettings(@RequestBody JsonNode settings) throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + + UserSettings userSettings = new UserSettings(); + userSettings.setSettings(settings); + userSettings.setUserId(currentUser.getId()); + return userSettingsService.updateUserSettings(currentUser.getTenantId(), userSettings).getSettings(); + } + + @ApiOperation(value = "Get user settings (getUserSettings)", + notes = "Fetch the User settings based on the provided User Id. " ) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/user/settings") + public JsonNode getUserSettings() throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + + UserSettings userSettings = userSettingsService.findUserSettings(currentUser.getTenantId(), currentUser.getId()); + return userSettings == null ? JacksonUtil.newObjectNode(): userSettings.getSettings(); + } + + @ApiOperation(value = "Delete user settings (deleteUserSettings)", + notes = "Delete user settings by specifying list of json element xpaths. \n " + + "Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter" ) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/user/settings/{jsonPaths}", method = RequestMethod.DELETE) + public void deleteUserSettings( @ApiParam(value = JSON_PATHS) + @PathVariable(JSON_PATHS) String jsonPaths) throws ThingsboardException { + checkParameter(USER_ID, jsonPaths); + + SecurityUser currentUser = getCurrentUser(); + userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), Arrays.asList(jsonPaths.split(","))); + } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 2fb92cdf4d..5549861950 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -457,6 +457,9 @@ cache: versionControlTask: timeToLiveInMinutes: "${CACHE_SPECS_VERSION_CONTROL_TASK_TTL:5}" maxSize: "${CACHE_SPECS_VERSION_CONTROL_TASK_MAX_SIZE:100000}" + userSettings: + timeToLiveInMinutes: "${CACHE_SPECS_USER_SETTINGS_TTL:1440}" + maxSize: "${CACHE_SPECS_USER_SETTINGS_MAX_SIZE:100000}" #Disable this because it is not required. spring.data.redis.repositories.enabled: false 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 9de2738a8e..24ce820760 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -103,6 +103,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; @@ -613,6 +614,22 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass); } + protected T doPut(String urlTemplate, T content, Class responseClass, String... params) { + try { + return readResponse(doPut(urlTemplate, content, params).andExpect(status().isOk()), responseClass); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + protected ResultActions doPut(String urlTemplate, T content, String... params) throws Exception { + MockHttpServletRequestBuilder postRequest = put(urlTemplate, params); + setJwtToken(postRequest); + String json = json(content); + postRequest.contentType(contentType).content(json); + return mockMvc.perform(postRequest); + } + protected T doDelete(String urlTemplate, Class responseClass, String... params) throws Exception { return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass); } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java index c7934d13aa..463c21ef8d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java @@ -29,6 +29,7 @@ import org.springframework.context.annotation.Primary; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.ResultActions; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; @@ -40,6 +41,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.UserSettings; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.user.UserDao; import org.thingsboard.server.service.mail.TestMailService; @@ -741,6 +743,48 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { assertThat(getErrorMessage(result)).containsIgnoringCase("invalid sort property"); } + @Test + public void testSaveUserSettings() throws Exception { + loginCustomerUser(); + + JsonNode userSettings = mapper.readTree("{\"A\":5, \"B\":10, \"E\":18}"); + JsonNode savedSettings = doPost("/api/user/settings", userSettings, JsonNode.class); + Assert.assertEquals(userSettings, savedSettings); + + JsonNode retrievedSettings = doGet("/api/user/settings", JsonNode.class); + Assert.assertEquals(retrievedSettings, userSettings); + } + + @Test + public void testUpdateUserSettings() throws Exception { + loginCustomerUser(); + + JsonNode userSettings = mapper.readTree("{\"A\":5, \"B\":10, \"E\":18}"); + JsonNode savedSettings = doPost("/api/user/settings", userSettings, JsonNode.class); + Assert.assertEquals(userSettings, savedSettings); + + JsonNode newSettings = mapper.readTree("{\"A\":10, \"B\":10, \"C\":{\"D\": 16}}"); + JsonNode updatedSettings = doPut("/api/user/settings", newSettings, JsonNode.class); + + JsonNode expectedSettings = mapper.readTree("{\"A\":10, \"B\":10, \"C\":{\"D\": 16}, \"E\":18}"); + Assert.assertEquals(expectedSettings, updatedSettings); + } + + @Test + public void testDeleteUserSettings() throws Exception { + loginCustomerUser(); + + JsonNode userSettings = mapper.readTree("{\"A\":10, \"B\":10, \"C\":{\"D\": 16}}"); + JsonNode savedSettings = doPost("/api/user/settings", userSettings, JsonNode.class); + Assert.assertEquals(userSettings, savedSettings); + + doDelete("/api/user/settings/C.D,B"); + + JsonNode retrievedSettings = doGet("/api/user/settings", JsonNode.class); + JsonNode expectedSettings = mapper.readTree("{\"A\":10, \"C\":{}}"); + Assert.assertEquals(expectedSettings, retrievedSettings); + } + private User createUser() throws Exception { loginSysAdmin(); String email = "tenant2@thingsboard.org"; diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserSettingsService.java new file mode 100644 index 0000000000..5ed1eab92a --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserSettingsService.java @@ -0,0 +1,43 @@ +/** + * 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.dao.user; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserCredentialsId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.dao.entity.EntityDaoService; + +import java.util.List; + +public interface UserSettingsService { + + UserSettings updateUserSettings(TenantId tenantId, UserSettings userSettings); + + UserSettings saveUserSettings(TenantId tenantId, UserSettings userSettings); + + UserSettings findUserSettings(TenantId tenantId, UserId userId); + + void deleteUserSettings(TenantId tenantId, UserId userId, List jsonPaths); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 7cfccb2c3c..c9e6e0aa46 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -39,4 +39,5 @@ public class CacheConstants { public static final String AUTO_COMMIT_SETTINGS_CACHE = "autoCommitSettings"; public static final String TWO_FA_VERIFICATION_CODES_CACHE = "twoFaVerificationCodes"; public static final String VERSION_CONTROL_TASK_CACHE = "versionControlTask"; + public static final String USER_SETTINGS_CACHE = "userSettings"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserSettings.java new file mode 100644 index 0000000000..5a7fb19ffa --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserSettings.java @@ -0,0 +1,61 @@ +/** + * 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.security; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.Serializable; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.getJson; +import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.setJson; + +@ApiModel +@Data +public class UserSettings implements Serializable { + + private static final long serialVersionUID = 2628320657987010348L; + + @ApiModelProperty(position = 1, value = "JSON object with User id.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private UserId userId; + + @ApiModelProperty(position = 2, value = "JSON object with user settings.", dataType = "com.fasterxml.jackson.databind.JsonNode") + @NoXss + @Length(fieldName = "settings", max = 100000) + private transient JsonNode settings; + + @JsonIgnore + private byte[] settingsBytes; + + public JsonNode getSettings() { + return getJson(() -> settings, () -> settingsBytes); + } + + public void setSettings(JsonNode settings) { + setJson(settings, json -> this.settings = json, bytes -> this.settingsBytes = bytes); + } +} diff --git a/dao/pom.xml b/dao/pom.xml index 901d967817..30dc1adcf0 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -233,6 +233,10 @@ org.eclipse.leshan leshan-core + + com.jayway.jsonpath + json-path + diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedService.java new file mode 100644 index 0000000000..d48acb9fc4 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedService.java @@ -0,0 +1,43 @@ +/** + * 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.dao.entity; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.thingsboard.server.cache.TbTransactionalCache; + +import java.io.Serializable; + +public abstract class AbstractCachedService { + + @Autowired + protected TbTransactionalCache cache; + + @Autowired + private ApplicationEventPublisher eventPublisher; + + protected void publishEvictEvent(E event) { + if (TransactionSynchronizationManager.isActualTransactionActive()) { + eventPublisher.publishEvent(event); + } else { + handleEvictEvent(event); + } + } + + public abstract void handleEvictEvent(E event); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 03c692f037..cf953583d1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -86,6 +86,13 @@ public class ModelConstants { public static final String USER_CREDENTIALS_BY_ACTIVATE_TOKEN_COLUMN_FAMILY_NAME = "user_credentials_by_activate_token"; public static final String USER_CREDENTIALS_BY_RESET_TOKEN_COLUMN_FAMILY_NAME = "user_credentials_by_reset_token"; + /** + * User settings constants. + */ + public static final String USER_SETTINGS_COLUMN_FAMILY_NAME = "user_settings"; + public static final String USER_SETTINGS_USER_ID_PROPERTY = USER_ID_PROPERTY; + public static final String USER_SETTINGS_SETTINGS = "settings"; + /** * Cassandra admin_settings constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserSettingsEntity.java new file mode 100644 index 0000000000..d7abf7ea23 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserSettingsEntity.java @@ -0,0 +1,66 @@ +/** + * 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.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.ToData; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import java.util.UUID; + +@Data +@NoArgsConstructor +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Entity +@Table(name = ModelConstants.USER_SETTINGS_COLUMN_FAMILY_NAME) +public class UserSettingsEntity implements ToData { + + @Id + @Column(name = ModelConstants.USER_SETTINGS_USER_ID_PROPERTY) + private UUID userId; + @Type(type = "json") + @Column(name = ModelConstants.USER_SETTINGS_SETTINGS) + private JsonNode settings; + + public UserSettingsEntity(UserSettings userSettings) { + this.userId = userSettings.getUserId().getId(); + if (userSettings.getSettings() != null) { + this.settings= userSettings.getSettings(); + } + } + + @Override + public UserSettings toData() { + UserSettings userSettings = new UserSettings(); + userSettings.setUserId(new UserId(userId)); + if (settings != null) { + userSettings.setSettings(settings); + } + return userSettings; + } + +} 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 new file mode 100644 index 0000000000..13e8564206 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java @@ -0,0 +1,53 @@ +/** + * 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.dao.sql.user; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserSettings; +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; + +@Slf4j +@Component +@SqlDao +public class JpaUserSettingsDao extends JpaAbstractDaoListeningExecutorService implements UserSettingsDao { + + @Autowired + private UserSettingsRepository userSettingsRepository; + + @Override + public UserSettings save(TenantId tenantId, UserSettings userSettings) { + return DaoUtil.getData(userSettingsRepository.save(new UserSettingsEntity(userSettings))); + } + + @Override + public UserSettings findById(TenantId tenantId, UserId userId) { + return DaoUtil.getData(userSettingsRepository.findById(userId.getId())); + } + + @Override + public void removeById(TenantId tenantId, UserId userId) { + userSettingsRepository.deleteById(userId.getId()); + } + +} 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 new file mode 100644 index 0000000000..6b3e28abc9 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java @@ -0,0 +1,25 @@ +/** + * 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.dao.sql.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.thingsboard.server.dao.model.sql.UserSettingsEntity; + +import java.util.UUID; + +public interface UserSettingsRepository extends JpaRepository { + +} 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 f5c00f038b..c8c5175e5f 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 @@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.data.security.UserSettings; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.IncorrectParameterException; @@ -76,6 +77,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic private final UserDao userDao; private final UserCredentialsDao userCredentialsDao; private final UserAuthSettingsDao userAuthSettingsDao; + private final UserSettingsDao userSettingsDao; private final DataValidator userValidator; private final DataValidator userCredentialsValidator; private final ApplicationEventPublisher eventPublisher; diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsCaffeineCache.java new file mode 100644 index 0000000000..3154e5d8c0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsCaffeineCache.java @@ -0,0 +1,36 @@ +/** + * 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.dao.user; + +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.asset.Asset; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.dao.asset.AssetCacheKey; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("UserSettingsCache") +public class UserSettingsCaffeineCache extends CaffeineTbTransactionalCache { + + public UserSettingsCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.USER_SETTINGS_CACHE); + } + +} 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 new file mode 100644 index 0000000000..16a7675a64 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java @@ -0,0 +1,30 @@ +/** + * 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.dao.user; + +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserSettings; + +public interface UserSettingsDao { + + UserSettings save(TenantId tenantId, UserSettings userSettings); + + UserSettings findById(TenantId tenantId, UserId userId); + + void removeById(TenantId tenantId, UserId userId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsEvictEvent.java new file mode 100644 index 0000000000..e0d2de7ece --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsEvictEvent.java @@ -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.dao.user; + +import lombok.Data; +import org.thingsboard.server.common.data.id.UserId; + +@Data +public class UserSettingsEvictEvent { + private final UserId userId; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsRedisCache.java new file mode 100644 index 0000000000..a510e6c864 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsRedisCache.java @@ -0,0 +1,38 @@ +/** + * 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.dao.user; + +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.TbFSTRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.dao.asset.AssetCacheKey; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("UserSettingsCache") +public class UserSettingsRedisCache extends RedisTbTransactionalCache { + + public UserSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.USER_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>()); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java new file mode 100644 index 0000000000..88087d28b1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java @@ -0,0 +1,131 @@ +/** + * 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.dao.user; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.dao.entity.AbstractCachedService; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Service("UserSettingsDaoService") +@Slf4j +@RequiredArgsConstructor +public class UserSettingsServiceImpl extends AbstractCachedService implements UserSettingsService { + public static final String INCORRECT_USER_ID = "Incorrect userId "; + private final UserSettingsDao userSettingsDao; + + @Override + public UserSettings saveUserSettings(TenantId tenantId, UserSettings userSettings) { + log.trace("Executing saveUserSettings for user [{}], [{}]", userSettings.getUserId(), userSettings); + validateId(userSettings.getUserId(), INCORRECT_USER_ID + userSettings.getUserId()); + try { + UserSettings saved = userSettingsDao.save(tenantId, userSettings); + publishEvictEvent(new UserSettingsEvictEvent(userSettings.getUserId())); + return saved; + } catch (Exception t) { + handleEvictEvent(new UserSettingsEvictEvent(userSettings.getUserId())); + throw t; + } + } + + @Override + public UserSettings updateUserSettings(TenantId tenantId, UserSettings userSettings) { + log.trace("Executing updateUserSettings for user [{}], [{}]", userSettings.getUserId(), userSettings); + validateId(userSettings.getUserId(), INCORRECT_USER_ID + userSettings.getUserId()); + UserSettings oldSettings = userSettingsDao.findById(tenantId, userSettings.getUserId()); + userSettings.setSettings(merge(oldSettings.getSettings(), userSettings.getSettings())); + + try { + UserSettings saved = userSettingsDao.save(tenantId, userSettings); + publishEvictEvent(new UserSettingsEvictEvent(userSettings.getUserId())); + return saved; + } catch (Exception t) { + handleEvictEvent(new UserSettingsEvictEvent(userSettings.getUserId())); + throw t; + } + } + + @Override + public UserSettings findUserSettings(TenantId tenantId, UserId userId) { + log.trace("Executing findUserSettings for user [{}]", userId); + validateId(userId, INCORRECT_USER_ID + userId); + + return cache.getAndPutInTransaction(userId, + () -> userSettingsDao.findById(tenantId, userId), true); + } + + @Override + public void deleteUserSettings(TenantId tenantId, UserId userId, List jsonPaths) { + log.trace("Executing deleteUserSettings for user [{}]", userId); + validateId(userId, INCORRECT_USER_ID + userId); + UserSettings userSettings = userSettingsDao.findById(tenantId, userId); + DocumentContext docSettings = JsonPath.parse(userSettings.getSettings().toString()); + try { + for (String s : jsonPaths) { + docSettings = docSettings.delete("$." + s); + } + userSettings.setSettings(new ObjectMapper().readValue(docSettings.jsonString(), ObjectNode.class)); + userSettingsDao.save(tenantId, userSettings); + publishEvictEvent(new UserSettingsEvictEvent(userSettings.getUserId())); + } catch (Exception t) { + handleEvictEvent(new UserSettingsEvictEvent(userSettings.getUserId())); + throw new RuntimeException(t); + } + } + + @TransactionalEventListener(classes = UserSettingsEvictEvent.class) + @Override + public void handleEvictEvent(UserSettingsEvictEvent event) { + List keys = new ArrayList<>(); + keys.add(event.getUserId()); + cache.evict(keys); + } + + public JsonNode merge(JsonNode mainNode, JsonNode updateNode) { + + Iterator fieldNames = updateNode.fieldNames(); + while (fieldNames.hasNext()) { + String fieldName = fieldNames.next(); + JsonNode jsonNode = mainNode.get(fieldName); + if (jsonNode != null && jsonNode.isObject()) { + merge(jsonNode, updateNode.get(fieldName)); + } + else { + if (mainNode instanceof ObjectNode) { + JsonNode value = updateNode.get(fieldName); + ((ObjectNode) mainNode).set(fieldName, value); + } + } + } + return mainNode; + } + +} diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 209a1dc806..b222cf4b96 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -789,4 +789,10 @@ CREATE TABLE IF NOT EXISTS user_auth_settings ( created_time bigint NOT NULL, user_id uuid UNIQUE NOT NULL CONSTRAINT fk_user_auth_settings_user_id REFERENCES tb_user(id), two_fa_settings varchar +); + +CREATE TABLE IF NOT EXISTS user_settings ( + user_id uuid NOT NULL CONSTRAINT user_settings_pkey PRIMARY KEY, + settings varchar(10000), + CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE ); \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java index 6ad50508d7..d7885a1cb2 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java @@ -19,16 +19,19 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.data.security.UserSettings; import org.thingsboard.server.dao.exception.DataValidationException; import java.util.ArrayList; @@ -40,6 +43,7 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { private IdComparator idComparator = new IdComparator<>(); private TenantId tenantId; + private UserSettings userSettings; @Before public void before() { @@ -65,7 +69,9 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { customerUser.setTenantId(tenantId); customerUser.setCustomerId(savedCustomer.getId()); customerUser.setEmail("customer@thingsboard.org"); - userService.saveUser(customerUser); + customerUser = userService.saveUser(customerUser); + + userSettings = createUserSettings(customerUser.getId()); } @After @@ -474,4 +480,11 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { tenantService.deleteTenant(tenantId); } + private UserSettings createUserSettings(UserId userId) { + UserSettings userSettings = new UserSettings(); + userSettings.setUserId(userId); + userSettings.setSettings(JacksonUtil.newObjectNode().put("text", StringUtils.randomAlphanumeric(10))); + return userSettings; + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDaoTest.java new file mode 100644 index 0000000000..4260221a73 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDaoTest.java @@ -0,0 +1,101 @@ +/** + * 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.dao.sql.user; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.dao.AbstractJpaDaoTest; +import org.thingsboard.server.dao.service.AbstractServiceTest; +import org.thingsboard.server.dao.user.UserDao; +import org.thingsboard.server.dao.user.UserSettingsDao; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; +import static org.thingsboard.server.dao.service.AbstractServiceTest.SYSTEM_TENANT_ID; + +public class JpaUserSettingsDaoTest extends AbstractJpaDaoTest { + + private UUID tenantId; + private User user; + + @Autowired + private UserSettingsDao userSettingsDao; + + @Autowired + private UserDao userDao; + + @Before + public void setUp() { + tenantId = Uuids.timeBased(); + user = saveUser(tenantId, Uuids.timeBased()); + } + + @After + public void tearDown() { + userDao.removeById(user.getTenantId(), user.getUuidId()); + } + + @Test + public void testFindSettingsByUserId() { + UserSettings userSettings = createUserSettings(user.getId()); + + UserSettings retrievedUserSettings = userSettingsDao.findById(SYSTEM_TENANT_ID, user.getId()); + assertEquals(retrievedUserSettings.getSettings(), userSettings.getSettings()); + + userSettingsDao.removeById(SYSTEM_TENANT_ID, user.getId()); + + UserSettings retrievedUserSettings2 = userSettingsDao.findById(SYSTEM_TENANT_ID, user.getId()); + assertNull(retrievedUserSettings2); + } + + private UserSettings createUserSettings(UserId userId) { + UserSettings userSettings = new UserSettings(); + userSettings.setSettings(JacksonUtil.newObjectNode().put("text", RandomStringUtils.randomAlphanumeric(10))); + userSettings.setUserId(userId); + return userSettingsDao.save(SYSTEM_TENANT_ID, userSettings); + } + + private User saveUser(UUID tenantId, UUID customerId) { + User user = new User(); + UUID id = Uuids.timeBased(); + user.setId(new UserId(id)); + user.setTenantId(TenantId.fromUUID(tenantId)); + user.setCustomerId(new CustomerId(customerId)); + if (customerId == NULL_UUID) { + user.setAuthority(Authority.TENANT_ADMIN); + } else { + user.setAuthority(Authority.CUSTOMER_USER); + } + String idString = id.toString(); + String email = idString.substring(0, idString.indexOf('-')) + "@thingsboard.org"; + user.setEmail(email); + return userDao.save(AbstractServiceTest.SYSTEM_TENANT_ID, user); + } +}