Added userLogin tracking

- Added new table for login tracking
- Added entity
- Added logic for login tracking
This commit is contained in:
Max W. 2024-06-01 14:08:11 +02:00
parent 4977dbe98a
commit d50d6de466
8 changed files with 78 additions and 11 deletions

View File

@ -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(); initializeAdminUser();
log.info("Database ready for operation!"); log.info("Database ready for operation!");

View File

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

View File

@ -49,6 +49,7 @@ public class UserRepository {
// If username is changed, this method must be used. Else the user will not be found // If username is changed, this method must be used. Else the user will not be found
public void updateUser(User user, String originalUsername) { public void updateUser(User user, String originalUsername) {
// TODO: Refactor this to use the userID instead of the username
r.db(config.getDatabase()).table("users") r.db(config.getDatabase()).table("users")
.filter(r.hashMap("username", originalUsername)) .filter(r.hashMap("username", originalUsername))
.update(user) .update(user)

View File

@ -13,6 +13,7 @@ import java.util.Date;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class User { public class User {
private String id; // ID is auto mapped by RethinkDB
private String username; private String username;
private String password; private String password;
private String email; private String email;

View File

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

View File

@ -2,6 +2,7 @@ package de.w665.sharepulse.rest.mappings;
import de.w665.sharepulse.rest.ro.AuthenticationRequest; import de.w665.sharepulse.rest.ro.AuthenticationRequest;
import de.w665.sharepulse.service.AuthenticationService; import de.w665.sharepulse.service.AuthenticationService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -24,9 +25,9 @@ public class AuthenticationController {
} }
@PostMapping("/login") @PostMapping("/login")
public ResponseEntity<Object> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) { public ResponseEntity<Object> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest, HttpServletRequest request) {
log.debug("Received AuthenticationRequest for username: " + authenticationRequest.getUsername()); 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<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();
response.put("token", token); response.put("token", token);

View File

@ -1,7 +1,9 @@
package de.w665.sharepulse.service; package de.w665.sharepulse.service;
import de.w665.sharepulse.db.repo.UserLoginRepository;
import de.w665.sharepulse.db.repo.UserRepository; import de.w665.sharepulse.db.repo.UserRepository;
import de.w665.sharepulse.model.User; import de.w665.sharepulse.model.User;
import de.w665.sharepulse.model.UserLogin;
import io.jsonwebtoken.*; import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
@ -20,6 +22,7 @@ import java.util.Optional;
public class AuthenticationService { public class AuthenticationService {
private final UserRepository userRepository; private final UserRepository userRepository;
private final UserLoginRepository userLoginRepository;
@Value("${secureapi.jwt.secret}") @Value("${secureapi.jwt.secret}")
private String secretString; private String secretString;
@ -29,8 +32,9 @@ public class AuthenticationService {
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
public AuthenticationService(UserRepository userRepository) { public AuthenticationService(UserRepository userRepository, UserLoginRepository userLoginRepository) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.userLoginRepository = userLoginRepository;
} }
@PostConstruct @PostConstruct
@ -40,14 +44,16 @@ public class AuthenticationService {
this.secretKey = Keys.hmacShaKeyFor(encodedKey); 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) { if(expirationTime.length > 0) {
this.expirationTime = expirationTime[0]; this.expirationTime = expirationTime[0];
} }
Optional<User> user = userRepository.retrieveUserByUsername(username); Optional<User> optionalUser = userRepository.retrieveUserByUsername(username);
if (user.isPresent() && passwordEncoder.matches(password, user.get().getPassword())) { if (optionalUser.isPresent() && passwordEncoder.matches(password, optionalUser.get().getPassword())) {
userRepository.updateLastLoginForUser(user.get().getUsername(), new Date()); User user = optionalUser.get();
return generateToken(user.get()); userLoginRepository.insertUserLogin(new UserLogin(user.getId(), new Date(), remoteAddr));
userRepository.updateLastLoginForUser(user.getUsername(), new Date());
return generateToken(user);
} }
return null; return null;
} }

View File

@ -38,13 +38,13 @@ public class AuthenticationServiceTest {
@Test @Test
public void whenValidUsernameAndPassword_thenAuthenticateShouldReturnToken() { 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"); assertNotNull(token, "Token should not be null for valid credentials");
} }
@Test @Test
public void whenValidToken_thenValidateTokenShouldReturnTrue() { 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"); assertTrue(authenticationService.validateToken(token), "Token validation should return true for a valid token");
} }
@ -57,7 +57,7 @@ public class AuthenticationServiceTest {
@Test @Test
public void whenTokenIsExplicitlyExpired_thenValidateTokenShouldReturnFalse() throws InterruptedException { public void whenTokenIsExplicitlyExpired_thenValidateTokenShouldReturnFalse() throws InterruptedException {
long testExpirationTime = 1; // 1 millisecond 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"); assertNotNull(token, "Token should not be null");
Thread.sleep(2); // Wait for 2 milliseconds to ensure the token has expired (Bad practice but easy) 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"); assertFalse(authenticationService.validateToken(token), "Expired token should not be valid");