feat(auth): POST /auth/login retourne email, name, role et token
CI — Tests & Docker Build / Tests (push) Failing after 4s
CI — Tests & Docker Build / Build & push image Docker (push) Has been skipped

- Champ `name` ajouté sur User, UserEntity, RegisterRequest
- AuthenticateUserUseCase retourne Result(user, token) au lieu du token seul
- UserNotFoundException remplacé par BadCredentialsException au login (pas de fuite d'info)
- @Email retiré de LoginRequest (identifiant = "gato", pas nécessairement un email)
- Migration V2 : colonne name + utilisateur par défaut gato/change (ADMIN)
- bytecode cible Java 21 (ASM Spring Boot 3.4 ne supporte pas Java 25)
- Tests : AbstractIntegrationTest simplifié, URL TC JDBC + network host

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 08:24:41 +02:00
parent c34cc41496
commit 12a28af1ca
19 changed files with 64 additions and 42 deletions
@@ -1,7 +1,10 @@
package com.olhar.olharapi.application.port.in;
import com.olhar.olharapi.domain.model.User;
public interface AuthenticateUserUseCase {
String authenticate(Command command);
Result authenticate(Command command);
record Command(String email, String password) {}
record Result(User user, String token) {}
}
@@ -5,5 +5,5 @@ import com.olhar.olharapi.domain.model.User;
public interface RegisterUserUseCase {
User register(Command command);
record Command(String email, String password) {}
record Command(String email, String name, String password) {}
}
@@ -2,7 +2,6 @@ package com.olhar.olharapi.application.usecase;
import com.olhar.olharapi.application.port.in.AuthenticateUserUseCase;
import com.olhar.olharapi.application.port.out.UserRepository;
import com.olhar.olharapi.domain.exception.UserNotFoundException;
import com.olhar.olharapi.domain.model.User;
import com.olhar.olharapi.infrastructure.security.JwtService;
import lombok.RequiredArgsConstructor;
@@ -19,14 +18,14 @@ public class AuthenticateUserService implements AuthenticateUserUseCase {
private final JwtService jwtService;
@Override
public String authenticate(Command command) {
public Result authenticate(Command command) {
User user = userRepository.findByEmail(command.email())
.orElseThrow(() -> new UserNotFoundException(command.email()));
.orElseThrow(() -> new BadCredentialsException("Identifiants invalides"));
if (!passwordEncoder.matches(command.password(), user.passwordHash())) {
throw new BadCredentialsException("Invalid credentials");
throw new BadCredentialsException("Identifiants invalides");
}
return jwtService.generateToken(user);
return new Result(user, jwtService.generateToken(user));
}
}
@@ -29,6 +29,7 @@ public class RegisterUserService implements RegisterUserUseCase {
User user = new User(
UUID.randomUUID(),
command.email(),
command.name(),
passwordEncoder.encode(command.password()),
User.Role.USER,
Instant.now()
@@ -6,11 +6,16 @@ import java.util.UUID;
public record User(
UUID id,
String email,
String name,
String passwordHash,
Role role,
Instant createdAt
) {
public enum Role {
USER, ADMIN
USER, ADMIN;
public String toApiRole() {
return this == ADMIN ? "admin" : "member";
}
}
}
@@ -21,6 +21,9 @@ public class UserEntity {
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String name;
@Column(name = "password_hash", nullable = false)
private String passwordHash;
@@ -11,6 +11,7 @@ public class UserPersistenceMapper {
return new User(
entity.getId(),
entity.getEmail(),
entity.getName(),
entity.getPasswordHash(),
User.Role.valueOf(entity.getRole().name()),
entity.getCreatedAt()
@@ -21,6 +22,7 @@ public class UserPersistenceMapper {
return UserEntity.builder()
.id(user.id())
.email(user.email())
.name(user.name())
.passwordHash(user.passwordHash())
.role(UserEntity.RoleEnum.valueOf(user.role().name()))
.createdAt(user.createdAt())
@@ -30,7 +30,7 @@ public class AuthController {
@Operation(summary = "Créer un compte utilisateur")
public UserResponse register(@Valid @RequestBody RegisterRequest request) {
User user = registerUserUseCase.register(
new RegisterUserUseCase.Command(request.email(), request.password())
new RegisterUserUseCase.Command(request.email(), request.name(), request.password())
);
return userRestMapper.toResponse(user);
}
@@ -38,9 +38,10 @@ public class AuthController {
@PostMapping("/login")
@Operation(summary = "S'authentifier et obtenir un token JWT")
public AuthResponse login(@Valid @RequestBody LoginRequest request) {
String token = authenticateUserUseCase.authenticate(
AuthenticateUserUseCase.Result result = authenticateUserUseCase.authenticate(
new AuthenticateUserUseCase.Command(request.email(), request.password())
);
return new AuthResponse(token);
User user = result.user();
return new AuthResponse(user.email(), user.name(), user.role().toApiRole(), result.token());
}
}
@@ -4,6 +4,6 @@ import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public record LoginRequest(
@Email @NotBlank String email,
@NotBlank String email,
@NotBlank String password
) {}
@@ -6,5 +6,6 @@ import jakarta.validation.constraints.Size;
public record RegisterRequest(
@Email @NotBlank String email,
@NotBlank String name,
@NotBlank @Size(min = 8) String password
) {}
@@ -1,3 +1,3 @@
package com.olhar.olharapi.interfaces.rest.dto.response;
public record AuthResponse(String token) {}
public record AuthResponse(String email, String name, String role, String token) {}
@@ -3,4 +3,4 @@ package com.olhar.olharapi.interfaces.rest.dto.response;
import java.time.Instant;
import java.util.UUID;
public record UserResponse(UUID id, String email, String role, Instant createdAt) {}
public record UserResponse(UUID id, String email, String name, String role, Instant createdAt) {}
@@ -11,7 +11,8 @@ public class UserRestMapper {
return new UserResponse(
user.id(),
user.email(),
user.role().name(),
user.name(),
user.role().toApiRole(),
user.createdAt()
);
}
@@ -0,0 +1,11 @@
ALTER TABLE users ADD COLUMN name VARCHAR(255) NOT NULL DEFAULT '';
INSERT INTO users (id, email, name, password_hash, role, created_at)
VALUES (
gen_random_uuid(),
'gato',
'Gato',
'$2a$10$53jNzmKK21BVNrhrAToFouWoO/agW3TVe8j7nusbrYRVqALp/MALK',
'ADMIN',
now()
) ON CONFLICT (email) DO NOTHING;
@@ -2,28 +2,10 @@ package com.olhar.olharapi.infrastructure;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@Testcontainers
public abstract class AbstractIntegrationTest {
@Container
static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
.withDatabaseName("olhar_test")
.withUsername("olhar")
.withPassword("olhar");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver");
}
// Le container PostgreSQL est géré automatiquement via l'URL TC JDBC
// définie dans application-test.yml : jdbc:tc:postgresql:16:///olhar_test
}
@@ -23,7 +23,7 @@ class JwtServiceTest {
@Test
void generateToken_shouldReturnValidToken() {
User user = new User(UUID.randomUUID(), "user@example.com", "hash", User.Role.USER, Instant.now());
User user = new User(UUID.randomUUID(), "user@example.com", "Test User", "hash", User.Role.USER, Instant.now());
String token = jwtService.generateToken(user);
@@ -18,7 +18,7 @@ class AuthControllerIT extends AbstractIntegrationTest {
@Test
void register_shouldCreateUser_whenEmailIsNew() {
RegisterRequest request = new RegisterRequest("test@example.com", "password123");
RegisterRequest request = new RegisterRequest("test@example.com", "Test User", "password123");
ResponseEntity<String> response = restTemplate.postForEntity("/api/v1/auth/register", request, String.class);
@@ -28,7 +28,7 @@ class AuthControllerIT extends AbstractIntegrationTest {
@Test
void register_shouldReturnConflict_whenEmailAlreadyExists() {
RegisterRequest request = new RegisterRequest("duplicate@example.com", "password123");
RegisterRequest request = new RegisterRequest("duplicate@example.com", "Duplicate User", "password123");
restTemplate.postForEntity("/api/v1/auth/register", request, String.class);
ResponseEntity<String> response = restTemplate.postForEntity("/api/v1/auth/register", request, String.class);
@@ -38,7 +38,7 @@ class AuthControllerIT extends AbstractIntegrationTest {
@Test
void login_shouldReturnToken_whenCredentialsAreValid() {
RegisterRequest registerRequest = new RegisterRequest("login@example.com", "password123");
RegisterRequest registerRequest = new RegisterRequest("login@example.com", "Login User", "password123");
restTemplate.postForEntity("/api/v1/auth/register", registerRequest, String.class);
LoginRequest loginRequest = new LoginRequest("login@example.com", "password123");
@@ -50,7 +50,7 @@ class AuthControllerIT extends AbstractIntegrationTest {
@Test
void login_shouldReturnUnauthorized_whenPasswordIsWrong() {
RegisterRequest registerRequest = new RegisterRequest("wrongpass@example.com", "password123");
RegisterRequest registerRequest = new RegisterRequest("wrongpass@example.com", "Wrong Pass User", "password123");
restTemplate.postForEntity("/api/v1/auth/register", registerRequest, String.class);
LoginRequest loginRequest = new LoginRequest("wrongpass@example.com", "wrongpassword");