Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions CODE_REFACTOR_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ all identified issues with an aggressive approach to modernization.

## Phase 5: Security Enhancements (Days 10-11)

### 5.1 JWT Authentication Filter Improvements
### 5.1 JWT Authentication Filter Improvements

**File**: `JwtAuthenticationFilter.java`

Expand All @@ -224,7 +224,7 @@ all identified issues with an aggressive approach to modernization.

**Breaking Changes**: None (internal improvements)

### 5.2 JWT Entry Point
### 5.2 JWT Entry Point

**File**: `JwtAuthenticationEntryPoint.java`

Expand All @@ -240,7 +240,7 @@ all identified issues with an aggressive approach to modernization.

## Phase 6: Functional Programming Patterns (Days 12-13)

### 6.1 Mapper Enhancements
### 6.1 Mapper Enhancements

**File**: `UserMapper.java`

Expand All @@ -253,7 +253,7 @@ all identified issues with an aggressive approach to modernization.

**Breaking Changes**: Return type changes to Optional (BREAKING)

### 6.2 Service Method Signatures
### 6.2 Service Method Signatures

**Files**: All service classes

Expand All @@ -267,7 +267,7 @@ all identified issues with an aggressive approach to modernization.

**Breaking Changes**: Method signatures change (BREAKING)

### 6.3 DTO Enhancements
### 6.3 DTO Enhancements

**Files**: All DTO records

Expand Down
8 changes: 6 additions & 2 deletions src/main/java/org/nkcoder/dto/auth/LoginRequest.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package org.nkcoder.dto.auth;

import static org.nkcoder.validation.ValidationMessages.EMAIL_INVALID;
import static org.nkcoder.validation.ValidationMessages.EMAIL_REQUIRED;
import static org.nkcoder.validation.ValidationMessages.PASSWORD_REQUIRED;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record LoginRequest(
@NotBlank(message = "Email is required") @Email(message = "Please provide a valid email") String email,
@NotBlank(message = "Password is required") String password) {}
@NotBlank(message = EMAIL_REQUIRED) @Email(message = EMAIL_INVALID) String email,
@NotBlank(message = PASSWORD_REQUIRED) String password) {}
4 changes: 3 additions & 1 deletion src/main/java/org/nkcoder/dto/auth/RefreshTokenRequest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.nkcoder.dto.auth;

import static org.nkcoder.validation.ValidationMessages.REFRESH_TOKEN_REQUIRED;

import jakarta.validation.constraints.NotBlank;

public record RefreshTokenRequest(
@NotBlank(message = "Refresh token is required") String refreshToken) {}
@NotBlank(message = REFRESH_TOKEN_REQUIRED) String refreshToken) {}
34 changes: 25 additions & 9 deletions src/main/java/org/nkcoder/dto/auth/RegisterRequest.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package org.nkcoder.dto.auth;

import static org.nkcoder.validation.PasswordValidation.COMPLEXITY_PATTERN;
import static org.nkcoder.validation.PasswordValidation.MIN_LENGTH;
import static org.nkcoder.validation.PasswordValidation.PASSWORD_COMPLEXITY;
import static org.nkcoder.validation.PasswordValidation.PASSWORD_MIN_LENGTH;
import static org.nkcoder.validation.PasswordValidation.PASSWORD_REQUIRED;
import static org.nkcoder.validation.ValidationMessages.EMAIL_INVALID;
import static org.nkcoder.validation.ValidationMessages.EMAIL_REQUIRED;
import static org.nkcoder.validation.ValidationMessages.NAME_MAX_LENGTH;
import static org.nkcoder.validation.ValidationMessages.NAME_MIN_LENGTH;
import static org.nkcoder.validation.ValidationMessages.NAME_REQUIRED;
import static org.nkcoder.validation.ValidationMessages.NAME_SIZE;
import static org.nkcoder.validation.ValidationMessages.PASSWORD_COMPLEXITY;
import static org.nkcoder.validation.ValidationMessages.PASSWORD_COMPLEXITY_PATTERN;
import static org.nkcoder.validation.ValidationMessages.PASSWORD_MIN_LENGTH;
import static org.nkcoder.validation.ValidationMessages.PASSWORD_REQUIRED;
import static org.nkcoder.validation.ValidationMessages.PASSWORD_SIZE;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
Expand All @@ -13,7 +19,17 @@
import org.nkcoder.enums.Role;

public record RegisterRequest(
@NotBlank(message = "Email is required") @Email(message = "Please provide a valid email") String email,
@NotBlank(message = PASSWORD_REQUIRED) @Size(min = MIN_LENGTH, message = PASSWORD_MIN_LENGTH) @Pattern(regexp = COMPLEXITY_PATTERN, message = PASSWORD_COMPLEXITY) String password,
@NotBlank(message = "Name is required") @Size(min = 2, max = 50, message = "Name must be between 2 and 50 " + "characters") String name,
Role role) {}
@NotBlank(message = EMAIL_REQUIRED) @Email(message = EMAIL_INVALID) String email,
@NotBlank(message = PASSWORD_REQUIRED) @Size(min = PASSWORD_MIN_LENGTH, message = PASSWORD_SIZE) @Pattern(regexp = PASSWORD_COMPLEXITY_PATTERN, message = PASSWORD_COMPLEXITY) String password,
@NotBlank(message = NAME_REQUIRED) @Size(min = NAME_MIN_LENGTH, max = NAME_MAX_LENGTH, message = NAME_SIZE) String name,
Role role) {
// Compact constructor that normalizes email to lowercase
public RegisterRequest {
if (email != null) {
email = email.toLowerCase().trim();
}
if (name != null) {
name = name.trim();
}
}
}
16 changes: 8 additions & 8 deletions src/main/java/org/nkcoder/dto/user/ChangePasswordRequest.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package org.nkcoder.dto.user;

import static org.nkcoder.validation.PasswordValidation.COMPLEXITY_PATTERN;
import static org.nkcoder.validation.PasswordValidation.CONFIRM_PASSWORD_REQUIRED;
import static org.nkcoder.validation.PasswordValidation.CURRENT_PASSWORD_REQUIRED;
import static org.nkcoder.validation.PasswordValidation.MIN_LENGTH;
import static org.nkcoder.validation.PasswordValidation.PASSWORD_COMPLEXITY;
import static org.nkcoder.validation.PasswordValidation.PASSWORD_MIN_LENGTH;
import static org.nkcoder.validation.PasswordValidation.PASSWORD_REQUIRED;
import static org.nkcoder.validation.ValidationMessages.CONFIRM_PASSWORD_REQUIRED;
import static org.nkcoder.validation.ValidationMessages.CURRENT_PASSWORD_REQUIRED;
import static org.nkcoder.validation.ValidationMessages.PASSWORD_COMPLEXITY;
import static org.nkcoder.validation.ValidationMessages.PASSWORD_COMPLEXITY_PATTERN;
import static org.nkcoder.validation.ValidationMessages.PASSWORD_MIN_LENGTH;
import static org.nkcoder.validation.ValidationMessages.PASSWORD_REQUIRED;
import static org.nkcoder.validation.ValidationMessages.PASSWORD_SIZE;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
Expand All @@ -16,5 +16,5 @@
@PasswordMatch
public record ChangePasswordRequest(
@NotBlank(message = CURRENT_PASSWORD_REQUIRED) String currentPassword,
@NotBlank(message = PASSWORD_REQUIRED) @Size(min = MIN_LENGTH, message = PASSWORD_MIN_LENGTH) @Pattern(regexp = COMPLEXITY_PATTERN, message = PASSWORD_COMPLEXITY) String newPassword,
@NotBlank(message = PASSWORD_REQUIRED) @Size(min = PASSWORD_MIN_LENGTH, message = PASSWORD_SIZE) @Pattern(regexp = PASSWORD_COMPLEXITY_PATTERN, message = PASSWORD_COMPLEXITY) String newPassword,
@NotBlank(message = CONFIRM_PASSWORD_REQUIRED) String confirmPassword) {}
9 changes: 7 additions & 2 deletions src/main/java/org/nkcoder/dto/user/UpdateProfileRequest.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package org.nkcoder.dto.user;

import static org.nkcoder.validation.ValidationMessages.EMAIL_INVALID;
import static org.nkcoder.validation.ValidationMessages.NAME_MAX_LENGTH;
import static org.nkcoder.validation.ValidationMessages.NAME_MIN_LENGTH;
import static org.nkcoder.validation.ValidationMessages.NAME_SIZE;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Size;

public record UpdateProfileRequest(
@Email(message = "Please provide a valid email") String email,
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters") String name) {}
@Email(message = EMAIL_INVALID) String email,
@Size(min = NAME_MIN_LENGTH, max = NAME_MAX_LENGTH, message = NAME_SIZE) String name) {}
16 changes: 12 additions & 4 deletions src/main/java/org/nkcoder/mapper/UserMapper.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
package org.nkcoder.mapper;

import java.util.Objects;
import java.util.Optional;
import org.nkcoder.dto.user.UserResponse;
import org.nkcoder.entity.User;
import org.springframework.stereotype.Component;

@Component
public class UserMapper {

public UserResponse toResponse(User user) {
if (user == null) {
return null;
}
/** Converts a User entity to UserResponse, returning Optional.empty() is user is null. */
public Optional<UserResponse> toResponse(User user) {
return Optional.ofNullable(user).map(this::mapToResponse);
}

public UserResponse toResponseOrThrow(User user) {
Objects.requireNonNull(user, "User must not be null");
return mapToResponse(user);
}

private UserResponse mapToResponse(User user) {
return new UserResponse(
user.getId(),
user.getEmail(),
Expand Down
47 changes: 25 additions & 22 deletions src/main/java/org/nkcoder/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ public class AuthService {

private static final Logger logger = LoggerFactory.getLogger(AuthService.class);

public static final String USER_ALREADY_EXISTS = "User already exists";
public static final String INVALID_CREDENTIALS = "Invalid email or password";
public static final String INVALID_REFRESH_TOKEN = "Invalid refresh token";
public static final String REFRESH_TOKEN_EXPIRED = "Refresh token expired";
public static final String USER_NOT_FOUND = "User not found";

private final UserRepository userRepository;
private final RefreshTokenRepository refreshTokenRepository;
private final PasswordEncoder passwordEncoder;
Expand Down Expand Up @@ -59,7 +65,7 @@ public AuthResponse register(RegisterRequest request) {

// Check if user already exists
if (userRepository.existsByEmail(request.email().toLowerCase())) {
throw new ValidationException("User already exists");
throw new ValidationException(USER_ALREADY_EXISTS);
}

// Create new user
Expand All @@ -81,7 +87,7 @@ public AuthResponse register(RegisterRequest request) {
// Save refresh token
saveRefreshToken(tokens.refreshToken(), savedUser.getId(), tokenFamily);

return new AuthResponse(userMapper.toResponse(savedUser), tokens);
return new AuthResponse(userMapper.toResponseOrThrow(savedUser), tokens);
}

@Transactional
Expand All @@ -92,11 +98,11 @@ public AuthResponse login(LoginRequest request) {
User user =
userRepository
.findByEmail(request.email().toLowerCase())
.orElseThrow(() -> new AuthenticationException("Invalid email or password"));
.orElseThrow(() -> new AuthenticationException(INVALID_CREDENTIALS));

// Check password
if (!passwordEncoder.matches(request.password(), user.getPassword())) {
throw new AuthenticationException("Invalid email or password");
throw new AuthenticationException(INVALID_CREDENTIALS);
}

// Update last login
Expand All @@ -110,7 +116,7 @@ public AuthResponse login(LoginRequest request) {
saveRefreshToken(tokens.refreshToken(), user.getId(), tokenFamily);

logger.debug("User logged in successfully: {}", user.getId());
return new AuthResponse(userMapper.toResponse(user), tokens);
return new AuthResponse(userMapper.toResponseOrThrow(user), tokens);
}

@Transactional(isolation = Isolation.SERIALIZABLE)
Expand All @@ -127,32 +133,28 @@ public AuthResponse refreshTokens(String refreshToken) {
RefreshToken storedToken =
refreshTokenRepository
.findByTokenForUpdate(refreshToken)
.orElseThrow(() -> new AuthenticationException("Invalid refresh token"));
.orElseThrow(() -> new AuthenticationException(INVALID_REFRESH_TOKEN));

// Check if token is expired
if (storedToken.isExpired()) {
refreshTokenRepository.deleteByToken(refreshToken);
throw new AuthenticationException("Refresh token expired");
throw new AuthenticationException(REFRESH_TOKEN_EXPIRED);
}

// Get user
User user =
userRepository
.findById(userId)
.orElseThrow(() -> new AuthenticationException("User not found"));
.orElseThrow(() -> new AuthenticationException(USER_NOT_FOUND));

// Delete old refresh token
refreshTokenRepository.deleteByToken(refreshToken);

// Generate new tokens with same token family
AuthTokens tokens = generateAuthTokens(user, tokenFamily);

// Save new refresh token
saveRefreshToken(tokens.refreshToken(), user.getId(), tokenFamily);

logger.debug("Tokens refreshed successfully for user: {}", userId);
return new AuthResponse(userMapper.toResponse(user), tokens);

return new AuthResponse(userMapper.toResponseOrThrow(user), tokens);
} catch (JwtException e) {
logger.error("Invalid refresh token: {}", e.getMessage());

Expand All @@ -163,22 +165,23 @@ public AuthResponse refreshTokens(String refreshToken) {
storedToken ->
refreshTokenRepository.deleteByTokenFamily(storedToken.getTokenFamily()));

throw new AuthenticationException("Invalid refresh token");
throw new AuthenticationException(INVALID_REFRESH_TOKEN);
}
}

@Transactional
public void logout(String refreshToken) {
logger.debug("Logging out user (all devices)");

// Get token data
RefreshToken storedToken = refreshTokenRepository.findByToken(refreshToken).orElse(null);
if (storedToken != null) {
// Delete entire token family (logout from all devices)
refreshTokenRepository.deleteByTokenFamily(storedToken.getTokenFamily());
logger.debug(
"Logged out from all devices for token family: {}", storedToken.getTokenFamily());
}
refreshTokenRepository
.findByToken(refreshToken)
.ifPresent(
storedToken -> {
// Delete entire token family (logout from all devices)
refreshTokenRepository.deleteByTokenFamily(storedToken.getTokenFamily());
logger.debug(
"Logged out from all devices for token family: {}", storedToken.getTokenFamily());
});
}

@Transactional
Expand Down
Loading
Loading