diff --git a/src/main/java/com/fredmaina/chatapp/Auth/Dtos/LoginRequest.java b/src/main/java/com/fredmaina/chatapp/Auth/Dtos/LoginRequest.java index 78fc23a..0a37708 100644 --- a/src/main/java/com/fredmaina/chatapp/Auth/Dtos/LoginRequest.java +++ b/src/main/java/com/fredmaina/chatapp/Auth/Dtos/LoginRequest.java @@ -2,8 +2,11 @@ import lombok.Data; +import java.time.Instant; + @Data public class LoginRequest { private String username; private String password; + private final Instant lastLoginAt = Instant.now(); } diff --git a/src/main/java/com/fredmaina/chatapp/Auth/Dtos/UserDto.java b/src/main/java/com/fredmaina/chatapp/Auth/Dtos/UserDto.java index e95173b..546de20 100644 --- a/src/main/java/com/fredmaina/chatapp/Auth/Dtos/UserDto.java +++ b/src/main/java/com/fredmaina/chatapp/Auth/Dtos/UserDto.java @@ -6,6 +6,7 @@ import lombok.Getter; import lombok.Setter; +import java.time.Instant; import java.util.UUID; @Getter @@ -18,6 +19,8 @@ public class UserDto { private String email; private String username; private Role role; + private Instant createdAt; + private Instant lastLoginAt; public UserDto(User user) { this.userId = user.getId(); @@ -26,5 +29,7 @@ public UserDto(User user) { this.email = user.getEmail(); this.username = user.getUsername(); this.role = user.getRole(); + this.createdAt = user.getCreatedAt(); + this.lastLoginAt = user.getLastLoginAt(); } } diff --git a/src/main/java/com/fredmaina/chatapp/Auth/Models/User.java b/src/main/java/com/fredmaina/chatapp/Auth/Models/User.java index 0717b9b..3651381 100644 --- a/src/main/java/com/fredmaina/chatapp/Auth/Models/User.java +++ b/src/main/java/com/fredmaina/chatapp/Auth/Models/User.java @@ -4,7 +4,9 @@ import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import java.time.Instant; import java.util.UUID; @Entity @@ -37,5 +39,12 @@ public class User { @Enumerated(EnumType.STRING) private Role role; + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private Instant createdAt; + + @Column(name = "last_login_at", nullable = false) + private Instant lastLoginAt; + private boolean verified; } diff --git a/src/main/java/com/fredmaina/chatapp/Auth/controllers/AuthController.java b/src/main/java/com/fredmaina/chatapp/Auth/controllers/AuthController.java index 33ac32d..0bba347 100644 --- a/src/main/java/com/fredmaina/chatapp/Auth/controllers/AuthController.java +++ b/src/main/java/com/fredmaina/chatapp/Auth/controllers/AuthController.java @@ -1,7 +1,10 @@ package com.fredmaina.chatapp.Auth.controllers; -import com.fredmaina.chatapp.Auth.Dtos.*; +import com.fredmaina.chatapp.Auth.Dtos.AuthResponse; +import com.fredmaina.chatapp.Auth.Dtos.GoogleOAuthRequest; +import com.fredmaina.chatapp.Auth.Dtos.LoginRequest; +import com.fredmaina.chatapp.Auth.Dtos.SignUpRequest; import com.fredmaina.chatapp.Auth.Models.User; import com.fredmaina.chatapp.Auth.Repositories.UserRepository; import com.fredmaina.chatapp.Auth.services.AuthService; @@ -32,7 +35,7 @@ public class AuthController { @PostMapping("/login") public ResponseEntity login(@RequestBody LoginRequest loginRequest) { AuthResponse authResponse = authService.login(loginRequest); - if(authResponse.isSuccess()){ + if (authResponse.isSuccess()) { return ResponseEntity.ok(authResponse); } return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(authResponse); @@ -42,7 +45,7 @@ public ResponseEntity login(@RequestBody LoginRequest loginRequest @PostMapping("/register") public ResponseEntity register(@Valid @RequestBody SignUpRequest signUpRequest) { AuthResponse authResponse = authService.signUp(signUpRequest); - if(authResponse.isSuccess()){ + if (authResponse.isSuccess()) { return ResponseEntity.status(HttpStatus.CREATED).body(authResponse); } if ("Username already exists (case-insensitive)".equals(authResponse.getMessage()) || "Email already exists".equals(authResponse.getMessage())) { @@ -50,6 +53,7 @@ public ResponseEntity register(@Valid @RequestBody SignUpRequest s } return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(authResponse); // General bad request for other issues } + @PostMapping("/oauth/google") public ResponseEntity googleOAuth(@RequestBody GoogleOAuthRequest request) { AuthResponse response = authService.handleGoogleOAuth(request.getCode(), request.getRedirectUri()); @@ -81,8 +85,9 @@ public ResponseEntity me(@RequestHeader("Authorization") String au .user(user) .build()); } + @PostMapping("/set-username") - public ResponseEntity setUsername(@RequestBody Map map) { + public ResponseEntity setUsername(@RequestBody Map map) { String email = map.get("email"); String username = map.get("username"); @@ -91,8 +96,8 @@ public ResponseEntity setUsername(@RequestBody Map .body(AuthResponse.builder().success(false).message("Email and username are required.").build()); } - AuthResponse authResponse = authService.setUsername(email,username); - if(authResponse.isSuccess()){ + AuthResponse authResponse = authService.setUsername(email, username); + if (authResponse.isSuccess()) { return ResponseEntity.ok(authResponse); } // Distinguish between user not found and username taken diff --git a/src/main/java/com/fredmaina/chatapp/Auth/services/AuthService.java b/src/main/java/com/fredmaina/chatapp/Auth/services/AuthService.java index 3364ab0..3f00e4c 100644 --- a/src/main/java/com/fredmaina/chatapp/Auth/services/AuthService.java +++ b/src/main/java/com/fredmaina/chatapp/Auth/services/AuthService.java @@ -2,7 +2,6 @@ import com.fredmaina.chatapp.Auth.Dtos.AuthResponse; -import com.fredmaina.chatapp.Auth.Dtos.GoogleOAuthRequest; import com.fredmaina.chatapp.Auth.Dtos.LoginRequest; import com.fredmaina.chatapp.Auth.Dtos.SignUpRequest; import com.fredmaina.chatapp.Auth.Models.Role; @@ -15,14 +14,13 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.*; -import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; -import java.util.HashMap; +import java.time.Instant; import java.util.Map; import java.util.Optional; @@ -37,7 +35,7 @@ public class AuthService { final RestTemplate restTemplate; @Autowired - AuthService (UserRepository userRepository, PasswordEncoder passwordEncoder, GoogleOAuthProperties googleOAuthProperties,RestTemplate restTemplate,JWTService jwtService) { + AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder, GoogleOAuthProperties googleOAuthProperties, RestTemplate restTemplate, JWTService jwtService) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; this.googleOAuthProperties = googleOAuthProperties; @@ -133,6 +131,10 @@ public AuthResponse login(LoginRequest loginRequest) { authResponse.setMessage("Login successful"); authResponse.setUser(user); authResponse.setToken(token); + + user.setLastLoginAt(loginRequest.getLastLoginAt()); + userRepository.save(user); + return authResponse; } @@ -202,6 +204,9 @@ public AuthResponse handleGoogleOAuth(String code, String redirectUri) { String token = jwtService.generateToken(user.getEmail()); + user.setLastLoginAt(Instant.now()); + userRepository.save(user); + return AuthResponse.builder() .success(true) .message("OAuth login successful") @@ -253,9 +258,10 @@ public AuthResponse setUsername(String email, String username) { .success(false) .build(); } + @Cacheable(value = "usernameCheck", key = "#username != null ? #username.toLowerCase() : 'null'") public Boolean checkUsernameExists(String username) { - log.info("checking if username: {} exists",username); + log.info("checking if username: {} exists", username); if (username == null) return false; return userRepository.existsByUsernameIgnoreCase(username); } diff --git a/src/main/resources/db/migration/V8__Alter_users_table_to_add_register_login_times.sql b/src/main/resources/db/migration/V8__Alter_users_table_to_add_register_login_times.sql new file mode 100644 index 0000000..4961cda --- /dev/null +++ b/src/main/resources/db/migration/V8__Alter_users_table_to_add_register_login_times.sql @@ -0,0 +1,2 @@ +ALTER TABLE users ADD COLUMN created_at TIMESTAMP WITH TIME ZONE NOT NULL; +ALTER TABLE users ADD COLUMN last_login_at TIMESTAMP WITH TIME ZONE; \ No newline at end of file diff --git a/src/test/java/com/fredmaina/chatapp/Auth/Services/AuthServiceTest.java b/src/test/java/com/fredmaina/chatapp/Auth/Services/AuthServiceTest.java index b77792a..5f4b527 100644 --- a/src/test/java/com/fredmaina/chatapp/Auth/Services/AuthServiceTest.java +++ b/src/test/java/com/fredmaina/chatapp/Auth/Services/AuthServiceTest.java @@ -3,14 +3,15 @@ import com.fredmaina.chatapp.Auth.Dtos.AuthResponse; import com.fredmaina.chatapp.Auth.Dtos.LoginRequest; import com.fredmaina.chatapp.Auth.Dtos.SignUpRequest; -import com.fredmaina.chatapp.Auth.Models.Role; import com.fredmaina.chatapp.Auth.Models.User; import com.fredmaina.chatapp.Auth.Repositories.UserRepository; import com.fredmaina.chatapp.Auth.services.AuthService; import com.fredmaina.chatapp.Auth.services.JWTService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.*; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.security.crypto.password.PasswordEncoder; @@ -106,7 +107,7 @@ void testLogin_success() { user.setPassword("encodedPassword"); user.setEmail("fred@example.com"); - when(userRepository.findByUsername("fredmaina123")).thenReturn(Optional.of(user)); + when(userRepository.findByUsernameIgnoreCase("fredmaina123")).thenReturn(Optional.of(user)); when(passwordEncoder.matches("mypassword", "encodedPassword")).thenReturn(true); when(jwtService.generateToken(user.getEmail())).thenReturn("jwt-token");