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

View File

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

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.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<Object> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) {
public ResponseEntity<Object> 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<String, Object> response = new HashMap<>();
response.put("token", token);

View File

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

View File

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