Public Access
Started with:
- Registration logic for new players - Extended User table with more information
This commit is contained in:
1 parent
dbc76e6158
commit
c224cbf679
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;
|
||||
}
|
||||
}
|
||||
+45
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package auth_service.domain.registration.controller.dto.response;
|
||||
|
||||
public record ErrorResponse(String code, String message) {
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package auth_service.domain.registration.exception;
|
||||
|
||||
public class EmailAlreadyInUseException extends RuntimeException {
|
||||
public EmailAlreadyInUseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package auth_service.domain.registration.exception;
|
||||
|
||||
public class InvalidInvitationCodeException extends RuntimeException {
|
||||
public InvalidInvitationCodeException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
+7
@@ -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;
|
||||
}
|
||||
+12
@@ -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);
|
||||
}
|
||||
+23
@@ -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 = "Us*kksUuRpTE2CQRsv+^k0n)ksR}X}?#Fo2D4d8ZKG";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user