From 12a28af1ca1146e0f5a2d70e899e67eca9942015 Mon Sep 17 00:00:00 2001 From: Gato Date: Sun, 7 Jun 2026 08:24:41 +0200 Subject: [PATCH] feat(auth): POST /auth/login retourne email, name, role et token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .claude/rules/tests.md | 11 ++++++++-- build.gradle | 6 +++++ .../port/in/AuthenticateUserUseCase.java | 5 ++++- .../port/in/RegisterUserUseCase.java | 2 +- .../usecase/AuthenticateUserService.java | 9 ++++---- .../usecase/RegisterUserService.java | 1 + .../com/olhar/olharapi/domain/model/User.java | 7 +++++- .../persistence/entity/UserEntity.java | 3 +++ .../mapper/UserPersistenceMapper.java | 2 ++ .../rest/controller/AuthController.java | 7 +++--- .../rest/dto/request/LoginRequest.java | 2 +- .../rest/dto/request/RegisterRequest.java | 1 + .../rest/dto/response/AuthResponse.java | 2 +- .../rest/dto/response/UserResponse.java | 2 +- .../rest/mapper/UserRestMapper.java | 3 ++- .../V2__add_name_and_default_user.sql | 11 ++++++++++ .../AbstractIntegrationTest.java | 22 ++----------------- .../security/JwtServiceTest.java | 2 +- .../olharapi/interfaces/AuthControllerIT.java | 8 +++---- 19 files changed, 64 insertions(+), 42 deletions(-) create mode 100644 src/main/resources/db/migration/V2__add_name_and_default_user.sql diff --git a/.claude/rules/tests.md b/.claude/rules/tests.md index 7e9d46f..3e3e510 100644 --- a/.claude/rules/tests.md +++ b/.claude/rules/tests.md @@ -29,17 +29,24 @@ Voir `java-env.md` (règles workspace) pour les contraintes. Commande standard : podman run --rm -v $(pwd):/workspace:Z -w /workspace eclipse-temurin:25-jdk ./gradlew test --no-daemon ``` -Testcontainers nécessite le socket Docker/Podman : +Testcontainers nécessite le socket Podman et le réseau hôte (DinD) : ```bash podman run --rm \ + --privileged \ + --network host \ -v $(pwd):/workspace:Z \ - -v /run/user/$(id -u)/podman/podman.sock:/var/run/docker.sock \ + -v /run/user/$(id -u)/podman/podman.sock:/var/run/docker.sock:Z \ + -e DOCKER_HOST=unix:///var/run/docker.sock \ + -e TESTCONTAINERS_RYUK_DISABLED=true \ -w /workspace \ eclipse-temurin:25-jdk \ ./gradlew test --no-daemon ``` +- `--network host` est obligatoire : le TC JDBC driver démarre PostgreSQL via le socket Podman et s'y connecte via `localhost:PORT` — sans host network, ce port n'est pas accessible depuis l'intérieur du container. +- `TESTCONTAINERS_RYUK_DISABLED=true` : Ryuk (le garbage collector de Testcontainers) ne fonctionne pas en DinD. + ## JaCoCo dans build.gradle ```groovy diff --git a/build.gradle b/build.gradle index daa73a8..0a33a40 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,12 @@ java { } } +// Spring Boot 3.4.x embarque ASM 9.7 qui supporte jusqu'au bytecode Java 23 (version 67). +// On compile avec le JDK 25 mais on cible Java 21 pour rester compatible avec le scanner Spring. +tasks.withType(JavaCompile).configureEach { + options.release = 21 +} + repositories { mavenCentral() } diff --git a/src/main/java/com/olhar/olharapi/application/port/in/AuthenticateUserUseCase.java b/src/main/java/com/olhar/olharapi/application/port/in/AuthenticateUserUseCase.java index 1185b29..b8d1665 100644 --- a/src/main/java/com/olhar/olharapi/application/port/in/AuthenticateUserUseCase.java +++ b/src/main/java/com/olhar/olharapi/application/port/in/AuthenticateUserUseCase.java @@ -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) {} } diff --git a/src/main/java/com/olhar/olharapi/application/port/in/RegisterUserUseCase.java b/src/main/java/com/olhar/olharapi/application/port/in/RegisterUserUseCase.java index 113edac..5413b06 100644 --- a/src/main/java/com/olhar/olharapi/application/port/in/RegisterUserUseCase.java +++ b/src/main/java/com/olhar/olharapi/application/port/in/RegisterUserUseCase.java @@ -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) {} } diff --git a/src/main/java/com/olhar/olharapi/application/usecase/AuthenticateUserService.java b/src/main/java/com/olhar/olharapi/application/usecase/AuthenticateUserService.java index 8e6ba1c..75dd695 100644 --- a/src/main/java/com/olhar/olharapi/application/usecase/AuthenticateUserService.java +++ b/src/main/java/com/olhar/olharapi/application/usecase/AuthenticateUserService.java @@ -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)); } } diff --git a/src/main/java/com/olhar/olharapi/application/usecase/RegisterUserService.java b/src/main/java/com/olhar/olharapi/application/usecase/RegisterUserService.java index d58c2e9..a844cd3 100644 --- a/src/main/java/com/olhar/olharapi/application/usecase/RegisterUserService.java +++ b/src/main/java/com/olhar/olharapi/application/usecase/RegisterUserService.java @@ -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() diff --git a/src/main/java/com/olhar/olharapi/domain/model/User.java b/src/main/java/com/olhar/olharapi/domain/model/User.java index 3bb2cd6..9422c73 100644 --- a/src/main/java/com/olhar/olharapi/domain/model/User.java +++ b/src/main/java/com/olhar/olharapi/domain/model/User.java @@ -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"; + } } } diff --git a/src/main/java/com/olhar/olharapi/infrastructure/persistence/entity/UserEntity.java b/src/main/java/com/olhar/olharapi/infrastructure/persistence/entity/UserEntity.java index af9c7f1..05cebba 100644 --- a/src/main/java/com/olhar/olharapi/infrastructure/persistence/entity/UserEntity.java +++ b/src/main/java/com/olhar/olharapi/infrastructure/persistence/entity/UserEntity.java @@ -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; diff --git a/src/main/java/com/olhar/olharapi/infrastructure/persistence/mapper/UserPersistenceMapper.java b/src/main/java/com/olhar/olharapi/infrastructure/persistence/mapper/UserPersistenceMapper.java index bf5eda3..52c787d 100644 --- a/src/main/java/com/olhar/olharapi/infrastructure/persistence/mapper/UserPersistenceMapper.java +++ b/src/main/java/com/olhar/olharapi/infrastructure/persistence/mapper/UserPersistenceMapper.java @@ -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()) diff --git a/src/main/java/com/olhar/olharapi/interfaces/rest/controller/AuthController.java b/src/main/java/com/olhar/olharapi/interfaces/rest/controller/AuthController.java index 68c18d3..0bc5f92 100644 --- a/src/main/java/com/olhar/olharapi/interfaces/rest/controller/AuthController.java +++ b/src/main/java/com/olhar/olharapi/interfaces/rest/controller/AuthController.java @@ -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()); } } diff --git a/src/main/java/com/olhar/olharapi/interfaces/rest/dto/request/LoginRequest.java b/src/main/java/com/olhar/olharapi/interfaces/rest/dto/request/LoginRequest.java index 137cb67..b309a95 100644 --- a/src/main/java/com/olhar/olharapi/interfaces/rest/dto/request/LoginRequest.java +++ b/src/main/java/com/olhar/olharapi/interfaces/rest/dto/request/LoginRequest.java @@ -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 ) {} diff --git a/src/main/java/com/olhar/olharapi/interfaces/rest/dto/request/RegisterRequest.java b/src/main/java/com/olhar/olharapi/interfaces/rest/dto/request/RegisterRequest.java index b2126d1..b83db65 100644 --- a/src/main/java/com/olhar/olharapi/interfaces/rest/dto/request/RegisterRequest.java +++ b/src/main/java/com/olhar/olharapi/interfaces/rest/dto/request/RegisterRequest.java @@ -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 ) {} diff --git a/src/main/java/com/olhar/olharapi/interfaces/rest/dto/response/AuthResponse.java b/src/main/java/com/olhar/olharapi/interfaces/rest/dto/response/AuthResponse.java index 5f9faf1..18d981c 100644 --- a/src/main/java/com/olhar/olharapi/interfaces/rest/dto/response/AuthResponse.java +++ b/src/main/java/com/olhar/olharapi/interfaces/rest/dto/response/AuthResponse.java @@ -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) {} diff --git a/src/main/java/com/olhar/olharapi/interfaces/rest/dto/response/UserResponse.java b/src/main/java/com/olhar/olharapi/interfaces/rest/dto/response/UserResponse.java index c78a96a..cd645b1 100644 --- a/src/main/java/com/olhar/olharapi/interfaces/rest/dto/response/UserResponse.java +++ b/src/main/java/com/olhar/olharapi/interfaces/rest/dto/response/UserResponse.java @@ -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) {} diff --git a/src/main/java/com/olhar/olharapi/interfaces/rest/mapper/UserRestMapper.java b/src/main/java/com/olhar/olharapi/interfaces/rest/mapper/UserRestMapper.java index 778b1d2..a83ad79 100644 --- a/src/main/java/com/olhar/olharapi/interfaces/rest/mapper/UserRestMapper.java +++ b/src/main/java/com/olhar/olharapi/interfaces/rest/mapper/UserRestMapper.java @@ -11,7 +11,8 @@ public class UserRestMapper { return new UserResponse( user.id(), user.email(), - user.role().name(), + user.name(), + user.role().toApiRole(), user.createdAt() ); } diff --git a/src/main/resources/db/migration/V2__add_name_and_default_user.sql b/src/main/resources/db/migration/V2__add_name_and_default_user.sql new file mode 100644 index 0000000..bea4030 --- /dev/null +++ b/src/main/resources/db/migration/V2__add_name_and_default_user.sql @@ -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; diff --git a/src/test/java/com/olhar/olharapi/infrastructure/AbstractIntegrationTest.java b/src/test/java/com/olhar/olharapi/infrastructure/AbstractIntegrationTest.java index 868b159..c0c58c0 100644 --- a/src/test/java/com/olhar/olharapi/infrastructure/AbstractIntegrationTest.java +++ b/src/test/java/com/olhar/olharapi/infrastructure/AbstractIntegrationTest.java @@ -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 } diff --git a/src/test/java/com/olhar/olharapi/infrastructure/security/JwtServiceTest.java b/src/test/java/com/olhar/olharapi/infrastructure/security/JwtServiceTest.java index 6eb8d33..6a5e5d0 100644 --- a/src/test/java/com/olhar/olharapi/infrastructure/security/JwtServiceTest.java +++ b/src/test/java/com/olhar/olharapi/infrastructure/security/JwtServiceTest.java @@ -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); diff --git a/src/test/java/com/olhar/olharapi/interfaces/AuthControllerIT.java b/src/test/java/com/olhar/olharapi/interfaces/AuthControllerIT.java index 1252a4c..63abb98 100644 --- a/src/test/java/com/olhar/olharapi/interfaces/AuthControllerIT.java +++ b/src/test/java/com/olhar/olharapi/interfaces/AuthControllerIT.java @@ -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 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 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");