Browse Source

Merge branch 'userSettings2' of github.com:dashevchenko/thingsboard into develop/3.5

feature/user-settings
Andrii Shvaika 4 years ago
parent
commit
92030e3fb1
  1. 6
      application/src/main/data/upgrade/3.4.4/schema_update.sql
  2. 4
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  3. 60
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  4. 3
      application/src/main/resources/thingsboard.yml
  5. 17
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  6. 44
      application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
  7. 43
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserSettingsService.java
  8. 1
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  9. 61
      common/data/src/main/java/org/thingsboard/server/common/data/security/UserSettings.java
  10. 4
      dao/pom.xml
  11. 43
      dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedService.java
  12. 7
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  13. 66
      dao/src/main/java/org/thingsboard/server/dao/model/sql/UserSettingsEntity.java
  14. 53
      dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java
  15. 25
      dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java
  16. 2
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  17. 36
      dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsCaffeineCache.java
  18. 30
      dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java
  19. 24
      dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsEvictEvent.java
  20. 38
      dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsRedisCache.java
  21. 131
      dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java
  22. 6
      dao/src/main/resources/sql/schema-entities.sql
  23. 15
      dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java
  24. 101
      dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDaoTest.java

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

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

60
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(",")));
}
}

3
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

17
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> T doPut(String urlTemplate, T content, Class<T> responseClass, String... params) {
try {
return readResponse(doPut(urlTemplate, content, params).andExpect(status().isOk()), responseClass);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected <T> 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> T doDelete(String urlTemplate, Class<T> responseClass, String... params) throws Exception {
return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass);
}

44
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";

43
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<String> jsonPaths);
}

1
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";
}

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

4
dao/pom.xml

@ -233,6 +233,10 @@
<groupId>org.eclipse.leshan</groupId>
<artifactId>leshan-core</artifactId>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>
</dependencies>
<build>
<plugins>

43
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<K extends Serializable, V extends Serializable, E> {
@Autowired
protected TbTransactionalCache<K, V> 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);
}

7
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.
*/

66
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<UserSettings> {
@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;
}
}

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

25
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<UserSettingsEntity, UUID> {
}

2
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<User> userValidator;
private final DataValidator<UserCredentials> userCredentialsValidator;
private final ApplicationEventPublisher eventPublisher;

36
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<UserId, UserSettings> {
public UserSettingsCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.USER_SETTINGS_CACHE);
}
}

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

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

38
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<UserId, UserSettings> {
public UserSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.USER_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>());
}
}

131
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<UserId, UserSettings, UserSettingsEvictEvent> 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<String> 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<UserId> keys = new ArrayList<>();
keys.add(event.getUserId());
cache.evict(keys);
}
public JsonNode merge(JsonNode mainNode, JsonNode updateNode) {
Iterator<String> 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;
}
}

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

15
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<User> 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;
}
}

101
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);
}
}
Loading…
Cancel
Save