diff --git a/application/src/main/data/upgrade/3.8.1/schema_update.sql b/application/src/main/data/upgrade/3.8.1/schema_update.sql index 6b87dc6dde..1084dd374f 100644 --- a/application/src/main/data/upgrade/3.8.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.1/schema_update.sql @@ -14,3 +14,13 @@ -- limitations under the License. -- +ALTER TABLE user_credentials ADD COLUMN IF NOT EXISTS last_login_ts BIGINT; +UPDATE user_credentials c SET last_login_ts = (SELECT (additional_info::json ->> 'lastLoginTs')::bigint FROM tb_user u WHERE u.id = c.user_id) + WHERE last_login_ts IS NULL; + +ALTER TABLE user_credentials ADD COLUMN IF NOT EXISTS failed_login_attempts INT; +UPDATE user_credentials c SET failed_login_attempts = (SELECT (additional_info::json ->> 'failedLoginAttempts')::int FROM tb_user u WHERE u.id = c.user_id) + WHERE failed_login_attempts IS NULL; + +UPDATE tb_user SET additional_info = (additional_info::jsonb - 'lastLoginTs' - 'failedLoginAttempts' - 'userCredentialsEnabled')::text + WHERE additional_info IS NOT NULL AND additional_info != 'null'; 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 306e06539c..da7d6243fc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.common.util.DonAsynchron; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; @@ -877,14 +878,18 @@ public abstract class BaseController { } protected void checkUserInfo(User user) throws ThingsboardException { + ObjectNode info; if (user.getAdditionalInfo() instanceof ObjectNode additionalInfo) { - checkDashboardInfo(additionalInfo); - - UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()); - if (userCredentials.isEnabled() && !additionalInfo.has("userCredentialsEnabled")) { - additionalInfo.put("userCredentialsEnabled", true); - } + info = additionalInfo; + checkDashboardInfo(info); + } else { + info = JacksonUtil.newObjectNode(); + user.setAdditionalInfo(info); } + + UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()); + info.put("userCredentialsEnabled", userCredentials.isEnabled()); + info.put("lastLoginTs", userCredentials.getLastLoginTs()); } protected void checkDashboardInfo(JsonNode additionalInfo) throws ThingsboardException { 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 51442fb13a..b953fbe8d2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -78,7 +78,6 @@ import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; -import org.thingsboard.server.service.security.system.SystemSecurityService; import java.util.ArrayList; import java.util.Arrays; @@ -123,7 +122,6 @@ public class UserController extends BaseController { private final MailService mailService; private final JwtTokenFactory tokenFactory; - private final SystemSecurityService systemSecurityService; private final ApplicationEventPublisher eventPublisher; private final TbUserService tbUserService; private final EntityQueryService entityQueryService; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index 17ceb17ed1..893bd13171 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -50,7 +50,6 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.RelationActionEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.tenant.TenantService; -import org.thingsboard.server.dao.user.UserServiceImpl; /** * This event listener does not support async event processing because relay on ThreadLocal @@ -226,13 +225,10 @@ public class EdgeEventSourcingListener { } private void cleanUpUserAdditionalInfo(User user) { - // reset FAILED_LOGIN_ATTEMPTS and LAST_LOGIN_TS - edge is not interested in this information if (user.getAdditionalInfo() instanceof NullNode) { user.setAdditionalInfo(null); } if (user.getAdditionalInfo() instanceof ObjectNode additionalInfo) { - additionalInfo.remove(UserServiceImpl.FAILED_LOGIN_ATTEMPTS); - additionalInfo.remove(UserServiceImpl.LAST_LOGIN_TS); if (additionalInfo.isEmpty()) { user.setAdditionalInfo(null); } else { diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index 6516bd7ce6..c969fa71ed 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -265,7 +265,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { } } if (actionType == ActionType.LOGIN && e == null) { - userService.setLastLoginTs(user.getTenantId(), user.getId()); + userService.updateLastLoginTs(user.getTenantId(), user.getId()); } auditLogService.logEntityAction( user.getTenantId(), user.getCustomerId(), user.getId(), diff --git a/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java index 817ded33e7..73182587fb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java @@ -27,6 +27,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.UserActivationLink; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.model.SecuritySettings; @@ -67,31 +68,30 @@ public class AuthControllerTest extends AbstractControllerTest { .andExpect(status().isUnauthorized()); loginSysAdmin(); - doGet("/api/auth/user") - .andExpect(status().isOk()) - .andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name()))) - .andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL))); + User user = getCurrentUser(); + assertThat(user.getAuthority()).isEqualTo(Authority.SYS_ADMIN); + assertThat(user.getEmail()).isEqualTo(SYS_ADMIN_EMAIL); loginTenantAdmin(); - doGet("/api/auth/user") - .andExpect(status().isOk()) - .andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name()))) - .andExpect(jsonPath("$.email", is(TENANT_ADMIN_EMAIL))); + user = getCurrentUser(); + assertThat(user.getAuthority()).isEqualTo(Authority.TENANT_ADMIN); + assertThat(user.getEmail()).isEqualTo(TENANT_ADMIN_EMAIL); loginCustomerUser(); - doGet("/api/auth/user") - .andExpect(status().isOk()) - .andExpect(jsonPath("$.authority", is(Authority.CUSTOMER_USER.name()))) - .andExpect(jsonPath("$.email", is(CUSTOMER_USER_EMAIL))); + user = getCurrentUser(); + assertThat(user.getAuthority()).isEqualTo(Authority.CUSTOMER_USER); + assertThat(user.getEmail()).isEqualTo(CUSTOMER_USER_EMAIL); + user = getUser(customerUserId); + assertThat(user.getAdditionalInfo().get("userCredentialsEnabled").asBoolean()).isTrue(); + assertThat(user.getAdditionalInfo().get("lastLoginTs").asLong()).isCloseTo(System.currentTimeMillis(), within(10000L)); } @Test public void testLoginLogout() throws Exception { loginSysAdmin(); - doGet("/api/auth/user") - .andExpect(status().isOk()) - .andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name()))) - .andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL))); + User user = getCurrentUser(); + assertThat(user.getAuthority()).isEqualTo(Authority.SYS_ADMIN); + assertThat(user.getEmail()).isEqualTo(SYS_ADMIN_EMAIL); TimeUnit.SECONDS.sleep(1); //We need to make sure that event for invalidating token was successfully processed @@ -102,19 +102,45 @@ public class AuthControllerTest extends AbstractControllerTest { resetTokens(); } + @Test + public void testFailedLogin() throws Exception { + int maxFailedLoginAttempts = 3; + loginSysAdmin(); + updateSecuritySettings(securitySettings -> { + securitySettings.setMaxFailedLoginAttempts(maxFailedLoginAttempts); + }); + loginTenantAdmin(); + + for (int i = 0; i < maxFailedLoginAttempts; i++) { + String error = getErrorMessage(doPost("/api/auth/login", + new LoginRequest(CUSTOMER_USER_EMAIL, "IncorrectPassword")) + .andExpect(status().isUnauthorized())); + assertThat(error).containsIgnoringCase("invalid username or password"); + } + + User user = getUser(customerUserId); + assertThat(user.getAdditionalInfo().get("userCredentialsEnabled").asBoolean()).isTrue(); + + String error = getErrorMessage(doPost("/api/auth/login", + new LoginRequest(CUSTOMER_USER_EMAIL, "IncorrectPassword4")) + .andExpect(status().isUnauthorized())); + assertThat(error).containsIgnoringCase("account is locked"); + + user = getUser(customerUserId); + assertThat(user.getAdditionalInfo().get("userCredentialsEnabled").asBoolean()).isFalse(); + } + @Test public void testRefreshToken() throws Exception { loginSysAdmin(); - doGet("/api/auth/user") - .andExpect(status().isOk()) - .andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name()))) - .andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL))); + User user = getCurrentUser(); + assertThat(user.getAuthority()).isEqualTo(Authority.SYS_ADMIN); + assertThat(user.getEmail()).isEqualTo(SYS_ADMIN_EMAIL); refreshToken(); - doGet("/api/auth/user") - .andExpect(status().isOk()) - .andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name()))) - .andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL))); + user = getCurrentUser(); + assertThat(user.getAuthority()).isEqualTo(Authority.SYS_ADMIN); + assertThat(user.getEmail()).isEqualTo(SYS_ADMIN_EMAIL); } @Test @@ -277,6 +303,14 @@ public class AuthControllerTest extends AbstractControllerTest { doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk()); } + private User getCurrentUser() throws Exception { + return doGet("/api/auth/user", User.class); + } + + private User getUser(UserId id) throws Exception { + return doGet("/api/user/" + id, User.class); + } + private String getActivationLink(User user) throws Exception { return doGet("/api/user/" + user.getId() + "/activationLink", String.class); } diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index 63703ae27b..7fa848f95d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -319,7 +319,9 @@ public class TwoFactorAuthTest extends AbstractControllerTest { assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS); assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username); }); - assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo() + + loginTenantAdmin(); + assertThat(doGet("/api/user/" + user.getId(), User.class).getAdditionalInfo() .get("lastLoginTs").asLong()) .isGreaterThan(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(3)); } diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java index 17085f3ca9..c42ecc4545 100644 --- a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java @@ -113,6 +113,7 @@ public class UserControllerTest extends AbstractControllerTest { Assert.assertEquals(email, savedUser.getEmail()); User foundUser = doGet("/api/user/" + savedUser.getId().getId().toString(), User.class); + foundUser.setAdditionalInfo(savedUser.getAdditionalInfo()); Assert.assertEquals(foundUser, savedUser); testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundUser, foundUser, @@ -265,6 +266,7 @@ public class UserControllerTest extends AbstractControllerTest { User savedUser = doPost("/api/user", user, User.class); User foundUser = doGet("/api/user/" + savedUser.getId().getId().toString(), User.class); Assert.assertNotNull(foundUser); + foundUser.setAdditionalInfo(savedUser.getAdditionalInfo()); Assert.assertEquals(savedUser, foundUser); } diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index 1c3b5ccb7a..52204f8fde 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -47,7 +47,7 @@ public class UserEdgeTest extends AbstractEdgeTest { @Test public void testCreateUpdateDeleteTenantUser() throws Exception { // create user - edgeImitator.expectMessageAmount(6); + edgeImitator.expectMessageAmount(3); User newTenantAdmin = new User(); newTenantAdmin.setAuthority(Authority.TENANT_ADMIN); newTenantAdmin.setTenantId(tenantId); @@ -55,9 +55,9 @@ public class UserEdgeTest extends AbstractEdgeTest { newTenantAdmin.setFirstName("Boris"); newTenantAdmin.setLastName("Johnson"); User savedTenantAdmin = createUser(newTenantAdmin, "tenant"); - Assert.assertTrue(edgeImitator.waitForMessages()); // wait 6 messages - x2 user update msg and x4 user credentials update msgs (create + authenticate user) - Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); - Assert.assertEquals(4, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); + Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user) + Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); + Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); Optional userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); Assert.assertTrue(userUpdateMsgOpt.isPresent()); UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); @@ -133,7 +133,7 @@ public class UserEdgeTest extends AbstractEdgeTest { Assert.assertTrue(edgeImitator.waitForMessages()); // create user - edgeImitator.expectMessageAmount(6); + edgeImitator.expectMessageAmount(3); User customerUser = new User(); customerUser.setAuthority(Authority.CUSTOMER_USER); customerUser.setTenantId(tenantId); @@ -142,9 +142,9 @@ public class UserEdgeTest extends AbstractEdgeTest { customerUser.setFirstName("John"); customerUser.setLastName("Edwards"); User savedCustomerUser = createUser(customerUser, "customer"); - Assert.assertTrue(edgeImitator.waitForMessages()); // wait 6 messages - x2 user update msg and x4 user credentials update msgs (create + authenticate user) - Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); - Assert.assertEquals(4, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); + Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user) + Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); + Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); Optional userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); Assert.assertTrue(userUpdateMsgOpt.isPresent()); UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index 8f22812cfc..586ae50f5d 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -97,7 +97,7 @@ public interface UserService extends EntityDaoService { int increaseFailedLoginAttempts(TenantId tenantId, UserId userId); - void setLastLoginTs(TenantId tenantId, UserId userId); + void updateLastLoginTs(TenantId tenantId, UserId userId); void saveMobileSession(TenantId tenantId, UserId userId, String mobileToken, MobileSessionInfo sessionInfo); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java index 1104ae2949..4a882795db 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java @@ -40,6 +40,8 @@ public class UserCredentials extends BaseDataWithAdditionalInfo private Long resetTokenExpTime; @Convert(converter = JsonConverter.class) - @Column(name = ModelConstants.USER_CREDENTIALS_ADDITIONAL_PROPERTY) + @Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY) private JsonNode additionalInfo; + @Column(name = ModelConstants.USER_CREDENTIALS_LAST_LOGIN_TS_PROPERTY) + private Long lastLoginTs; + + @Column(name = ModelConstants.USER_CREDENTIALS_FAILED_LOGIN_ATTEMPTS_PROPERTY) + private Integer failedLoginAttempts; + public UserCredentialsEntity() { super(); } public UserCredentialsEntity(UserCredentials userCredentials) { - if (userCredentials.getId() != null) { - this.setUuid(userCredentials.getId().getId()); - } - this.setCreatedTime(userCredentials.getCreatedTime()); + super(userCredentials); if (userCredentials.getUserId() != null) { this.userId = userCredentials.getUserId().getId(); } @@ -82,6 +85,8 @@ public final class UserCredentialsEntity extends BaseSqlEntity this.resetToken = userCredentials.getResetToken(); this.resetTokenExpTime = userCredentials.getResetTokenExpTime(); this.additionalInfo = userCredentials.getAdditionalInfo(); + this.lastLoginTs = userCredentials.getLastLoginTs(); + this.failedLoginAttempts = userCredentials.getFailedLoginAttempts(); } @Override @@ -98,6 +103,8 @@ public final class UserCredentialsEntity extends BaseSqlEntity userCredentials.setResetToken(resetToken); userCredentials.setResetTokenExpTime(resetTokenExpTime); userCredentials.setAdditionalInfo(additionalInfo); + userCredentials.setLastLoginTs(lastLoginTs); + userCredentials.setFailedLoginAttempts(failedLoginAttempts); return userCredentials; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserCredentialsDao.java index 1f502db792..f277475896 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserCredentialsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserCredentialsDao.java @@ -69,4 +69,19 @@ public class JpaUserCredentialsDao extends JpaAbstractDao { void removeByUserId(TenantId tenantId, UserId userId); + void setLastLoginTs(TenantId tenantId, UserId userId, long lastLoginTs); + + int incrementFailedLoginAttempts(TenantId tenantId, UserId userId); + + void setFailedLoginAttempts(TenantId tenantId, UserId userId, int failedLoginAttempts); + } 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 ba0d3d69f3..dc758b6b5d 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 @@ -17,9 +17,6 @@ package org.thingsboard.server.dao.user; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.BooleanNode; -import com.fasterxml.jackson.databind.node.IntNode; -import com.fasterxml.jackson.databind.node.LongNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; @@ -87,15 +84,10 @@ public class UserServiceImpl extends AbstractCachedEntityService INCORRECT_USER_ID + id); UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, userId.getId()); userCredentials.setEnabled(enabled); - saveUserCredentials(tenantId, userCredentials); - - User user = findUserById(tenantId, userId); - user.setAdditionalInfoField(USER_CREDENTIALS_ENABLED, BooleanNode.valueOf(enabled)); if (enabled) { - resetFailedLoginAttempts(user); + userCredentials.setFailedLoginAttempts(0); } - saveUser(tenantId, user); + saveUserCredentials(tenantId, userCredentials); } - @Override public void resetFailedLoginAttempts(TenantId tenantId, UserId userId) { - log.trace("Executing onUserLoginSuccessful [{}]", userId); - User user = findUserById(tenantId, userId); - resetFailedLoginAttempts(user); - saveUser(tenantId, user); - } - - private void resetFailedLoginAttempts(User user) { - user.setAdditionalInfoField(FAILED_LOGIN_ATTEMPTS, IntNode.valueOf(0)); + log.trace("Executing resetFailedLoginAttempts [{}]", userId); + userCredentialsDao.setFailedLoginAttempts(tenantId, userId, 0); } @Override - public void setLastLoginTs(TenantId tenantId, UserId userId) { - User user = findUserById(tenantId, userId); - user.setAdditionalInfoField(LAST_LOGIN_TS, new LongNode(System.currentTimeMillis())); - saveUser(tenantId, user); + public void updateLastLoginTs(TenantId tenantId, UserId userId) { + userCredentialsDao.setLastLoginTs(tenantId, userId, System.currentTimeMillis()); } @Override @@ -502,18 +482,8 @@ public class UserServiceImpl extends AbstractCachedEntityService