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
|
* @return An Optional containing the user if found
|
||||||
*/
|
*/
|
||||||
Optional<User> findByEmail(String email);
|
Optional<User> findByEmail(String email);
|
||||||
|
|
||||||
|
void updateLoginDate(long userId);
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,8 @@ import jakarta.persistence.PersistenceContext;
|
|||||||
import jakarta.persistence.TypedQuery;
|
import jakarta.persistence.TypedQuery;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
@@ -30,4 +32,14 @@ public class MinimalUserRepositoryImpl implements MinimalUserRepository {
|
|||||||
.findFirst()
|
.findFirst()
|
||||||
.map(EntityMapper::mapToModel);
|
.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 jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a user entity mapped to the 'users' table in PostgreSQL.
|
* Represents a user entity mapped to the 'users' table in PostgreSQL.
|
||||||
*/
|
*/
|
||||||
@@ -27,4 +29,7 @@ public class MinimalUserEntity {
|
|||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private boolean enabled;
|
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
|
* @return The authenticated User model
|
||||||
*/
|
*/
|
||||||
public User login(User userToLogin) {
|
public User login(User userToLogin) {
|
||||||
var userFromDatabase = minimalUserRepository.findByEmail(userToLogin.getEmail()).orElseThrow(() -> new UserNotFoundException("User not found with given email"));
|
var foundUser = minimalUserRepository.findByEmail(userToLogin.getEmail()).orElseThrow(() -> new UserNotFoundException("User not found with given email"));
|
||||||
var loginSuccessful = passwordEncoder.matches(userToLogin.getPassword(), userFromDatabase.getPassword());
|
var loginSuccessful = passwordEncoder.matches(userToLogin.getPassword(), foundUser.getPassword());
|
||||||
if (!loginSuccessful) {
|
if (!loginSuccessful) {
|
||||||
throw new InvalidCredentialsException("Invalid credentials");
|
throw new InvalidCredentialsException("Invalid credentials");
|
||||||
}
|
}
|
||||||
|
minimalUserRepository.updateLoginDate(foundUser.getId());
|
||||||
return userFromDatabase;
|
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 lombok.Builder;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Builder
|
@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()
|
return UserProfileResponse.builder()
|
||||||
.id(user.getId())
|
.id(user.getId())
|
||||||
.email(user.getEmail())
|
.email(user.getEmail())
|
||||||
|
.createdDate(user.getCreatedDate())
|
||||||
|
.lastLogin(user.getLastLogin())
|
||||||
|
.steamId(user.getSteamId())
|
||||||
.role("USER").build();
|
.role("USER").build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,15 @@ package auth_service.domain.user.model;
|
|||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
@Data
|
@Data
|
||||||
public class User {
|
public class User {
|
||||||
private long id;
|
private long id;
|
||||||
private String email;
|
private String email;
|
||||||
private String encryptedPassword;
|
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 jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a user entity mapped to the 'users' table in PostgreSQL.
|
* Represents a user entity mapped to the 'users' table in PostgreSQL.
|
||||||
*/
|
*/
|
||||||
@@ -27,4 +29,13 @@ public class UserEntity {
|
|||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private boolean enabled;
|
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.setId(entity.getId());
|
||||||
model.setEmail(entity.getEmail());
|
model.setEmail(entity.getEmail());
|
||||||
model.setEncryptedPassword(entity.getPassword());
|
model.setEncryptedPassword(entity.getPassword());
|
||||||
|
model.setLastLogin(entity.getLastLoginDate());
|
||||||
|
model.setCreatedDate(entity.getRegistrationDate());
|
||||||
|
model.setSteamId(entity.getSteamId());
|
||||||
}
|
}
|
||||||
|
|
||||||
return model;
|
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