From d50d6de4664297bd4778f59b53b59d55595dc5b2 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 1 Jun 2024 14:08:11 +0200 Subject: [PATCH] Added userLogin tracking - Added new table for login tracking - Added entity - Added logic for login tracking --- .../w665/sharepulse/db/RethinkDBService.java | 13 +++++++++ .../db/repo/UserLoginRepository.java | 27 +++++++++++++++++++ .../sharepulse/db/repo/UserRepository.java | 1 + .../java/de/w665/sharepulse/model/User.java | 1 + .../de/w665/sharepulse/model/UserLogin.java | 18 +++++++++++++ .../mappings/AuthenticationController.java | 5 ++-- .../service/AuthenticationService.java | 18 ++++++++----- .../sharepulse/AuthenticationServiceTest.java | 6 ++--- 8 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 src/main/java/de/w665/sharepulse/db/repo/UserLoginRepository.java create mode 100644 src/main/java/de/w665/sharepulse/model/UserLogin.java diff --git a/src/main/java/de/w665/sharepulse/db/RethinkDBService.java b/src/main/java/de/w665/sharepulse/db/RethinkDBService.java index 41d4179..e3a67f8 100644 --- a/src/main/java/de/w665/sharepulse/db/RethinkDBService.java +++ b/src/main/java/de/w665/sharepulse/db/RethinkDBService.java @@ -105,6 +105,19 @@ public class RethinkDBService { } } + // rethinkdb check if table user_logins exists + try { + r.db(config.getDatabase()).tableCreate("user_logins").run(connection).stream(); + log.debug("Table 'user_logins' created successfully."); + } catch (ReqlOpFailedError e) { + log.debug("Table 'user_logins' already exists."); + if(autoResetOnStartup) { + log.debug("Clearing content..."); + r.db(config.getDatabase()).table("user_logins").delete().run(connection); + log.debug("Table 'user_logins' cleared successfully."); + } + } + initializeAdminUser(); log.info("Database ready for operation!"); diff --git a/src/main/java/de/w665/sharepulse/db/repo/UserLoginRepository.java b/src/main/java/de/w665/sharepulse/db/repo/UserLoginRepository.java new file mode 100644 index 0000000..f616fca --- /dev/null +++ b/src/main/java/de/w665/sharepulse/db/repo/UserLoginRepository.java @@ -0,0 +1,27 @@ +package de.w665.sharepulse.db.repo; + +import com.rethinkdb.RethinkDB; +import com.rethinkdb.net.Connection; +import de.w665.sharepulse.db.RethinkDBConfig; +import de.w665.sharepulse.db.RethinkDBConnector; +import de.w665.sharepulse.model.UserLogin; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class UserLoginRepository { + private final RethinkDB r; + private final Connection connection; + private final RethinkDBConfig config; + private final String TABLE_NAME = "user_logins"; + @Autowired + public UserLoginRepository(RethinkDBConnector connector, RethinkDBConfig config) { + this.r = connector.getR(); + this.connection = connector.getConnection(); + this.config = config; + } + + public void insertUserLogin(UserLogin userLogin) { + r.db(config.getDatabase()).table(TABLE_NAME).insert(userLogin).run(connection); + } +} diff --git a/src/main/java/de/w665/sharepulse/db/repo/UserRepository.java b/src/main/java/de/w665/sharepulse/db/repo/UserRepository.java index 485e66d..94d2f25 100644 --- a/src/main/java/de/w665/sharepulse/db/repo/UserRepository.java +++ b/src/main/java/de/w665/sharepulse/db/repo/UserRepository.java @@ -49,6 +49,7 @@ public class UserRepository { // If username is changed, this method must be used. Else the user will not be found public void updateUser(User user, String originalUsername) { + // TODO: Refactor this to use the userID instead of the username r.db(config.getDatabase()).table("users") .filter(r.hashMap("username", originalUsername)) .update(user) diff --git a/src/main/java/de/w665/sharepulse/model/User.java b/src/main/java/de/w665/sharepulse/model/User.java index ec38466..124fb89 100644 --- a/src/main/java/de/w665/sharepulse/model/User.java +++ b/src/main/java/de/w665/sharepulse/model/User.java @@ -13,6 +13,7 @@ import java.util.Date; @NoArgsConstructor @AllArgsConstructor public class User { + private String id; // ID is auto mapped by RethinkDB private String username; private String password; private String email; diff --git a/src/main/java/de/w665/sharepulse/model/UserLogin.java b/src/main/java/de/w665/sharepulse/model/UserLogin.java new file mode 100644 index 0000000..5aba29b --- /dev/null +++ b/src/main/java/de/w665/sharepulse/model/UserLogin.java @@ -0,0 +1,18 @@ +package de.w665.sharepulse.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Date; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class UserLogin { + String userId; + Date loginTime; + String loginIp; +} diff --git a/src/main/java/de/w665/sharepulse/rest/mappings/AuthenticationController.java b/src/main/java/de/w665/sharepulse/rest/mappings/AuthenticationController.java index c94c3ad..0eaacb7 100644 --- a/src/main/java/de/w665/sharepulse/rest/mappings/AuthenticationController.java +++ b/src/main/java/de/w665/sharepulse/rest/mappings/AuthenticationController.java @@ -2,6 +2,7 @@ package de.w665.sharepulse.rest.mappings; import de.w665.sharepulse.rest.ro.AuthenticationRequest; import de.w665.sharepulse.service.AuthenticationService; +import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -24,9 +25,9 @@ public class AuthenticationController { } @PostMapping("/login") - public ResponseEntity createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) { + public ResponseEntity createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest, HttpServletRequest request) { log.debug("Received AuthenticationRequest for username: " + authenticationRequest.getUsername()); - String token = authenticationService.authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword()); + String token = authenticationService.authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword(), request.getRemoteAddr()); Map response = new HashMap<>(); response.put("token", token); diff --git a/src/main/java/de/w665/sharepulse/service/AuthenticationService.java b/src/main/java/de/w665/sharepulse/service/AuthenticationService.java index c620166..92d4b9c 100644 --- a/src/main/java/de/w665/sharepulse/service/AuthenticationService.java +++ b/src/main/java/de/w665/sharepulse/service/AuthenticationService.java @@ -1,7 +1,9 @@ package de.w665.sharepulse.service; +import de.w665.sharepulse.db.repo.UserLoginRepository; import de.w665.sharepulse.db.repo.UserRepository; import de.w665.sharepulse.model.User; +import de.w665.sharepulse.model.UserLogin; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; @@ -20,6 +22,7 @@ import java.util.Optional; public class AuthenticationService { private final UserRepository userRepository; + private final UserLoginRepository userLoginRepository; @Value("${secureapi.jwt.secret}") private String secretString; @@ -29,8 +32,9 @@ public class AuthenticationService { private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); - public AuthenticationService(UserRepository userRepository) { + public AuthenticationService(UserRepository userRepository, UserLoginRepository userLoginRepository) { this.userRepository = userRepository; + this.userLoginRepository = userLoginRepository; } @PostConstruct @@ -40,14 +44,16 @@ public class AuthenticationService { this.secretKey = Keys.hmacShaKeyFor(encodedKey); } - public String authenticate(String username, String password, long... expirationTime/*FOR TESTING VALIDITY*/) { + public String authenticate(String username, String password, String remoteAddr, long... expirationTime/*FOR TESTING VALIDITY*/) { if(expirationTime.length > 0) { this.expirationTime = expirationTime[0]; } - Optional user = userRepository.retrieveUserByUsername(username); - if (user.isPresent() && passwordEncoder.matches(password, user.get().getPassword())) { - userRepository.updateLastLoginForUser(user.get().getUsername(), new Date()); - return generateToken(user.get()); + Optional optionalUser = userRepository.retrieveUserByUsername(username); + if (optionalUser.isPresent() && passwordEncoder.matches(password, optionalUser.get().getPassword())) { + User user = optionalUser.get(); + userLoginRepository.insertUserLogin(new UserLogin(user.getId(), new Date(), remoteAddr)); + userRepository.updateLastLoginForUser(user.getUsername(), new Date()); + return generateToken(user); } return null; } diff --git a/src/test/java/de/w665/sharepulse/AuthenticationServiceTest.java b/src/test/java/de/w665/sharepulse/AuthenticationServiceTest.java index ec2b662..75ffeb4 100644 --- a/src/test/java/de/w665/sharepulse/AuthenticationServiceTest.java +++ b/src/test/java/de/w665/sharepulse/AuthenticationServiceTest.java @@ -38,13 +38,13 @@ public class AuthenticationServiceTest { @Test public void whenValidUsernameAndPassword_thenAuthenticateShouldReturnToken() { - String token = authenticationService.authenticate(username, password); + String token = authenticationService.authenticate(username, password, ""); assertNotNull(token, "Token should not be null for valid credentials"); } @Test public void whenValidToken_thenValidateTokenShouldReturnTrue() { - String token = authenticationService.authenticate(username, password); + String token = authenticationService.authenticate(username, password, ""); assertTrue(authenticationService.validateToken(token), "Token validation should return true for a valid token"); } @@ -57,7 +57,7 @@ public class AuthenticationServiceTest { @Test public void whenTokenIsExplicitlyExpired_thenValidateTokenShouldReturnFalse() throws InterruptedException { long testExpirationTime = 1; // 1 millisecond - String token = authenticationService.authenticate("testUser", "testPass", testExpirationTime); + String token = authenticationService.authenticate("testUser", "testPass", "", testExpirationTime); assertNotNull(token, "Token should not be null"); Thread.sleep(2); // Wait for 2 milliseconds to ensure the token has expired (Bad practice but easy) assertFalse(authenticationService.validateToken(token), "Expired token should not be valid");