Started with:

- Registration logic for new players
- Extended User table with more information
This commit is contained in:
lobo committed 2026-04-04 17:11:13 +02:00
1 parent ad30de4190
commit 5dcdf7bd61
22 files changed
+341 -5

No files matched your search

@@ -15,4 +15,6 @@ public interface MinimalUserRepository {
* @return An Optional containing the user if found
*/
Optional<User> findByEmail(String email);
void updateLoginDate(long userId);
}
@@ -8,6 +8,8 @@ import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import org.springframework.stereotype.Repository;
import java.time.Clock;
import java.time.LocalDateTime;
import java.util.Optional;
@Repository
@@ -30,4 +32,14 @@ public class MinimalUserRepositoryImpl implements MinimalUserRepository {
.findFirst()
.map(EntityMapper::mapToModel);
}
@Override
public void updateLoginDate(long userId) {
var userEntity = entityManager.find(MinimalUserEntity.class, userId);
if (userEntity != null) {
userEntity.setLastLoginDate(LocalDateTime.now(Clock.systemUTC()));
entityManager.flush();
}
}
}
@@ -3,6 +3,8 @@ package auth_service.domain.login.persistence.entities;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
/**
* Represents a user entity mapped to the 'users' table in PostgreSQL.
*/
@@ -27,4 +29,7 @@ public class MinimalUserEntity {
@Column(nullable = false)
private boolean enabled;
@Column(name = "last_login_date", nullable = false)
private LocalDateTime lastLoginDate;
}
@@ -22,12 +22,12 @@ public class LoginService {
* @return The authenticated User model
*/
public User login(User userToLogin) {
var userFromDatabase = minimalUserRepository.findByEmail(userToLogin.getEmail()).orElseThrow(() -> new UserNotFoundException("User not found with given email"));
var loginSuccessful = passwordEncoder.matches(userToLogin.getPassword(), userFromDatabase.getPassword());
var foundUser = minimalUserRepository.findByEmail(userToLogin.getEmail()).orElseThrow(() -> new UserNotFoundException("User not found with given email"));
var loginSuccessful = passwordEncoder.matches(userToLogin.getPassword(), foundUser.getPassword());
if (!loginSuccessful) {
throw new InvalidCredentialsException("Invalid credentials");
}
return userFromDatabase;
minimalUserRepository.updateLoginDate(foundUser.getId());
return foundUser;
}
}
@@ -0,0 +1,45 @@
package auth_service.domain.registration.controller;
import auth_service.domain.registration.controller.dto.response.ErrorResponse;
import auth_service.domain.registration.exception.EmailAlreadyInUseException;
import auth_service.domain.registration.exception.InvalidInvitationCodeException;
import auth_service.domain.registration.exception.SteamIdAlreadyRegisteredException;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@Order(value = 1)
@Slf4j
public class RegistrationExceptionController {
private static @NonNull ResponseEntity<ErrorResponse> response(ErrorResponse body, HttpStatus status) {
return ResponseEntity.status(status.value())
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.body(body);
}
@ExceptionHandler(EmailAlreadyInUseException.class)
public ResponseEntity<ErrorResponse> clientError(EmailAlreadyInUseException ex) {
var body = new ErrorResponse("EMAIL_ALREADY_IN_USE", ex.getMessage());
return response(body, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(InvalidInvitationCodeException.class)
public ResponseEntity<ErrorResponse> clientError(InvalidInvitationCodeException ex) {
var body = new ErrorResponse("INVALID_INVITATION_CODE", ex.getMessage());
return response(body, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(SteamIdAlreadyRegisteredException.class)
public ResponseEntity<ErrorResponse> clientError(SteamIdAlreadyRegisteredException ex) {
var body = new ErrorResponse("STEAM_ID_ALREADY_IN_USE", ex.getMessage());
return response(body, HttpStatus.BAD_REQUEST);
}
}
@@ -0,0 +1,4 @@
package auth_service.domain.registration.controller.dto.response;
public record ErrorResponse(String code, String message) {
}
@@ -0,0 +1,7 @@
package auth_service.domain.registration.exception;
public class EmailAlreadyInUseException extends RuntimeException {
public EmailAlreadyInUseException(String message) {
super(message);
}
}
@@ -0,0 +1,7 @@
package auth_service.domain.registration.exception;
public class InvalidInvitationCodeException extends RuntimeException {
public InvalidInvitationCodeException(String message) {
super(message);
}
}
@@ -0,0 +1,7 @@
package auth_service.domain.registration.exception;
public class SteamIdAlreadyRegisteredException extends RuntimeException {
public SteamIdAlreadyRegisteredException(String message) {
super(message);
}
}
@@ -0,0 +1,7 @@
package auth_service.domain.registration.model;
public record RegisteredUser(
String id,
String email
) {
}
@@ -0,0 +1,14 @@
package auth_service.domain.registration.model;
import lombok.Builder;
import lombok.Data;
@Builder
@Data
public class Registration {
private String email;
private String rawPassword;
private String encryptedPassword;
private String inviteCode;
private long steamId;
}
@@ -0,0 +1,12 @@
package auth_service.domain.registration.persistence;
import auth_service.domain.registration.model.RegisteredUser;
import auth_service.domain.registration.model.Registration;
public interface RegisteredUserRepository {
boolean existsByEmail(String email);
boolean existsBySteamId(long steamId);
RegisteredUser save(Registration registration);
}
@@ -0,0 +1,23 @@
package auth_service.domain.registration.persistence;
import auth_service.domain.registration.model.RegisteredUser;
import auth_service.domain.registration.model.Registration;
import org.springframework.stereotype.Repository;
@Repository
public class RegisteredUserRepositoryImpl implements RegisteredUserRepository {
@Override
public boolean existsByEmail(String email) {
return false;
}
@Override
public boolean existsBySteamId(long steamId) {
return false;
}
@Override
public RegisteredUser save(Registration registration) {
return null;
}
}
@@ -0,0 +1,19 @@
package auth_service.domain.registration.service;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CodeValidationService {
private static final String RANDOM_INVITATION_CODE = "test-invite-code";
public boolean isCodeValid(String code) {
if (code == null || code.isEmpty()) {
return false;
}
return code.equals(RANDOM_INVITATION_CODE);
}
}
@@ -0,0 +1,39 @@
package auth_service.domain.registration.service;
import auth_service.domain.registration.exception.EmailAlreadyInUseException;
import auth_service.domain.registration.exception.InvalidInvitationCodeException;
import auth_service.domain.registration.exception.SteamIdAlreadyRegisteredException;
import auth_service.domain.registration.model.RegisteredUser;
import auth_service.domain.registration.model.Registration;
import auth_service.domain.registration.persistence.RegisteredUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class RegistrationService {
private final CodeValidationService codeValidationService;
private final RegisteredUserRepository registeredUserRepository;
private final PasswordEncoder passwordEncoder;
public RegisteredUser register(Registration registration) {
if (!codeValidationService.isCodeValid(registration.getInviteCode())) {
throw new InvalidInvitationCodeException("The provided invite code is invalid or has already been used.");
}
if (registeredUserRepository.existsByEmail(registration.getEmail())) {
throw new EmailAlreadyInUseException("The provided email address is already in use.");
}
if (registeredUserRepository.existsBySteamId(registration.getSteamId())) {
throw new SteamIdAlreadyRegisteredException("The steam id is already in use.");
}
var rawPassword = registration.getRawPassword();
registration.setEncryptedPassword(passwordEncoder.encode(rawPassword));
return registeredUserRepository.save(registration);
}
}
@@ -2,6 +2,8 @@ package auth_service.domain.user.controller.dto.response;
import lombok.Builder;
import java.time.LocalDateTime;
@Builder
public record UserProfileResponse(long id, String email, String role) {
public record UserProfileResponse(long id, String email, String role, LocalDateTime lastLogin, LocalDateTime createdDate, long steamId) {
}
@@ -12,6 +12,9 @@ public abstract class ApiMapper {
return UserProfileResponse.builder()
.id(user.getId())
.email(user.getEmail())
.createdDate(user.getCreatedDate())
.lastLogin(user.getLastLogin())
.steamId(user.getSteamId())
.role("USER").build();
}
}
@@ -3,10 +3,15 @@ package auth_service.domain.user.model;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
@Builder
@Data
public class User {
private long id;
private String email;
private String encryptedPassword;
private LocalDateTime lastLogin;
private LocalDateTime createdDate;
private long steamId;
}
@@ -3,6 +3,8 @@ package auth_service.domain.user.persistence.entities;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
/**
* Represents a user entity mapped to the 'users' table in PostgreSQL.
*/
@@ -27,4 +29,13 @@ public class UserEntity {
@Column(nullable = false)
private boolean enabled;
@Column(name = "steam_id", unique = true, nullable = false)
private Long steamId;
@Column(name = "registration_date", nullable = false)
private LocalDateTime registrationDate;
@Column(name = "last_login_date", nullable = false)
private LocalDateTime lastLoginDate;
}
@@ -15,6 +15,9 @@ public abstract class EntityMapper {
model.setId(entity.getId());
model.setEmail(entity.getEmail());
model.setEncryptedPassword(entity.getPassword());
model.setLastLogin(entity.getLastLoginDate());
model.setCreatedDate(entity.getRegistrationDate());
model.setSteamId(entity.getSteamId());
}
return model;
@@ -0,0 +1,7 @@
-- Create the users table
ALTER TABLE users
ADD COLUMN steam_id BIGINT,
ADD COLUMN registration_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN last_login_date TIMESTAMP;
ALTER TABLE users ADD CONSTRAINT uc_steam_id UNIQUE (steam_id);
@@ -0,0 +1,102 @@
package auth_service.domain.registration.service;
import auth_service.domain.registration.exception.EmailAlreadyInUseException;
import auth_service.domain.registration.exception.InvalidInvitationCodeException;
import auth_service.domain.registration.exception.SteamIdAlreadyRegisteredException;
import auth_service.domain.registration.model.RegisteredUser;
import auth_service.domain.registration.model.Registration;
import auth_service.domain.registration.persistence.RegisteredUserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class RegistrationServiceTest {
@Mock
private CodeValidationService codeValidationService;
@Mock
private RegisteredUserRepository registeredUserRepository;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private RegistrationService registrationService;
private Registration validRegistration;
@BeforeEach
void setUp() {
validRegistration = Registration.builder()
.email("test@example.com")
.rawPassword("SuperSecret123!")
.encryptedPassword("")
.inviteCode("TBP-INVITE-2026")
.steamId(314159L)
.build();
}
@Test
void register_withValidData_shouldReturnRegisteredUserAndMarkCodeAsUsed() {
// Arrange
when(codeValidationService.isCodeValid("TBP-INVITE-2026")).thenReturn(true);
when(registeredUserRepository.existsByEmail("test@example.com")).thenReturn(false);
when(passwordEncoder.encode("SuperSecret123!")).thenReturn("hashedPasswordXYZ");
RegisteredUser expectedUser = new RegisteredUser("uuid-1234", "test@example.com");
when(registeredUserRepository.save(validRegistration)).thenReturn(expectedUser);
// Act
RegisteredUser result = registrationService.register(validRegistration);
// Assert
assertNotNull(result);
assertEquals("uuid-1234", result.id());
assertEquals("test@example.com", result.email());
}
@Test
void register_withInvalidInviteCode_shouldThrowException() {
// Arrange
when(codeValidationService.isCodeValid("TBP-INVITE-2026")).thenReturn(false);
// Act & Assert
assertThrows(InvalidInvitationCodeException.class, () -> registrationService.register(validRegistration));
verify(registeredUserRepository, never()).save(any());
}
@Test
void register_withExistingEmail_shouldThrowException() {
// Arrange
when(codeValidationService.isCodeValid("TBP-INVITE-2026")).thenReturn(true);
when(registeredUserRepository.existsByEmail("test@example.com")).thenReturn(true);
// Act & Assert
assertThrows(EmailAlreadyInUseException.class, () -> registrationService.register(validRegistration));
verify(registeredUserRepository, never()).save(any());
}
@Test
void register_withExistingSteamId_shouldThrowException() {
// Arrange
when(codeValidationService.isCodeValid("TBP-INVITE-2026")).thenReturn(true);
when(registeredUserRepository.existsByEmail("test@example.com")).thenReturn(false);
when(registeredUserRepository.existsBySteamId(314159L)).thenReturn(true);
// Act & Assert
assertThrows(SteamIdAlreadyRegisteredException.class, () -> registrationService.register(validRegistration));
verify(registeredUserRepository, never()).save(any());
}
}