Repository: Rateit-PM4-Org/Rateit
Pull request #158: User register service
Author: login: zillinic
Created: 2025-03-23T14:29:32Z | Merged: 2025-03-25T19:45:14Z

Gerne reviewen.

PW hashing funktioniert noch nicht korrekt, ergibt mit selbem cleanPW verschiedene Hashes.
Soweit ich sehe, wird bei jeder Registrierung ein neuer BCryptPasswordEncoder erstellt.


Diff:
diff --git a/api/src/main/java/ch/zhaw/rateit/api/config/WebsecurityConfig.java b/api/src/main/java/ch/zhaw/rateit/api/config/WebsecurityConfig.java
index 6de6e621..38d145ea 100644
--- a/api/src/main/java/ch/zhaw/rateit/api/config/WebsecurityConfig.java
+++ b/api/src/main/java/ch/zhaw/rateit/api/config/WebsecurityConfig.java
@@ -5,6 +5,7 @@
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.web.cors.CorsConfiguration;
 import org.springframework.web.cors.CorsConfigurationSource;
@@ -20,6 +21,7 @@ public class WebsecurityConfig {
     public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
         return http
                 .authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().permitAll())
+                .csrf(AbstractHttpConfigurer::disable)
                 .cors(httpSecurityCorsConfigurer -> httpSecurityCorsConfigurer.configurationSource(corsConfigurationSource()))
                 .build();
     }
diff --git a/api/src/main/java/ch/zhaw/rateit/api/exceptions/handler/UserExceptionHandler.java b/api/src/main/java/ch/zhaw/rateit/api/exceptions/handler/UserExceptionHandler.java
new file mode 100644
index 00000000..6ac31fa8
--- /dev/null
+++ b/api/src/main/java/ch/zhaw/rateit/api/exceptions/handler/UserExceptionHandler.java
@@ -0,0 +1,22 @@
+package ch.zhaw.rateit.api.exceptions.handler;
+
+import ch.zhaw.rateit.api.exceptions.types.DuplicateEmailUserException;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.util.Map;
+
+/**
+ * Controller to handle all user exceptions
+ *
+ * @author Nicolas Zillig
+ */
+@RestControllerAdvice
+public class UserExceptionHandler {
+
+    @ExceptionHandler(DuplicateEmailUserException.class)
+    public ResponseEntity<Map<String, String>> handleUserRegistration(DuplicateEmailUserException ex) {
+        return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
+    }
+}
diff --git a/api/src/main/java/ch/zhaw/rateit/api/exceptions/types/DuplicateEmailUserException.java b/api/src/main/java/ch/zhaw/rateit/api/exceptions/types/DuplicateEmailUserException.java
new file mode 100644
index 00000000..65806dab
--- /dev/null
+++ b/api/src/main/java/ch/zhaw/rateit/api/exceptions/types/DuplicateEmailUserException.java
@@ -0,0 +1,12 @@
+package ch.zhaw.rateit.api.exceptions.types;
+
+/**
+ * Exception for user registration
+ *
+ * @author Nicolas Zillig
+ */
+public class DuplicateEmailUserException extends RuntimeException {
+    public DuplicateEmailUserException(String message) {
+        super(message);
+    }
+}
diff --git a/api/src/main/java/ch/zhaw/rateit/api/test/TestController.java b/api/src/main/java/ch/zhaw/rateit/api/logic/test/TestController.java
similarity index 95%
rename from api/src/main/java/ch/zhaw/rateit/api/test/TestController.java
rename to api/src/main/java/ch/zhaw/rateit/api/logic/test/TestController.java
index 098f7449..5d752ae2 100644
--- a/api/src/main/java/ch/zhaw/rateit/api/test/TestController.java
+++ b/api/src/main/java/ch/zhaw/rateit/api/logic/test/TestController.java
@@ -1,4 +1,4 @@
-package ch.zhaw.rateit.api.test;
+package ch.zhaw.rateit.api.logic.test;
 
 import jakarta.annotation.PostConstruct;
 import org.springframework.beans.factory.annotation.Autowired;
diff --git a/api/src/main/java/ch/zhaw/rateit/api/test/TestEntity.java b/api/src/main/java/ch/zhaw/rateit/api/logic/test/TestEntity.java
similarity index 90%
rename from api/src/main/java/ch/zhaw/rateit/api/test/TestEntity.java
rename to api/src/main/java/ch/zhaw/rateit/api/logic/test/TestEntity.java
index c3150e72..6d2c6019 100644
--- a/api/src/main/java/ch/zhaw/rateit/api/test/TestEntity.java
+++ b/api/src/main/java/ch/zhaw/rateit/api/logic/test/TestEntity.java
@@ -1,4 +1,4 @@
-package ch.zhaw.rateit.api.test;
+package ch.zhaw.rateit.api.logic.test;
 
 import org.springframework.data.annotation.Id;
 
diff --git a/api/src/main/java/ch/zhaw/rateit/api/test/TestEntityRepository.java b/api/src/main/java/ch/zhaw/rateit/api/logic/test/TestEntityRepository.java
similarity index 85%
rename from api/src/main/java/ch/zhaw/rateit/api/test/TestEntityRepository.java
rename to api/src/main/java/ch/zhaw/rateit/api/logic/test/TestEntityRepository.java
index 9243dcb8..dead14e8 100644
--- a/api/src/main/java/ch/zhaw/rateit/api/test/TestEntityRepository.java
+++ b/api/src/main/java/ch/zhaw/rateit/api/logic/test/TestEntityRepository.java
@@ -1,4 +1,4 @@
-package ch.zhaw.rateit.api.test;
+package ch.zhaw.rateit.api.logic.test;
 
 import org.springframework.data.mongodb.repository.MongoRepository;
 
diff --git a/api/src/main/java/ch/zhaw/rateit/api/logic/user/PasswordEncrypterConfiguration.java b/api/src/main/java/ch/zhaw/rateit/api/logic/user/PasswordEncrypterConfiguration.java
new file mode 100644
index 00000000..fc83b81b
--- /dev/null
+++ b/api/src/main/java/ch/zhaw/rateit/api/logic/user/PasswordEncrypterConfiguration.java
@@ -0,0 +1,18 @@
+package ch.zhaw.rateit.api.logic.user;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+/**
+ * Class to configure password encoder globally.
+ *
+ * @author Nicolas Zillig
+ */
+@Configuration
+public class PasswordEncrypterConfiguration {
+    @Bean
+    public BCryptPasswordEncoder getPasswordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+}
diff --git a/api/src/main/java/ch/zhaw/rateit/api/logic/user/controller/UserController.java b/api/src/main/java/ch/zhaw/rateit/api/logic/user/controller/UserController.java
new file mode 100644
index 00000000..487162a3
--- /dev/null
+++ b/api/src/main/java/ch/zhaw/rateit/api/logic/user/controller/UserController.java
@@ -0,0 +1,30 @@
+package ch.zhaw.rateit.api.logic.user.controller;
+
+import ch.zhaw.rateit.api.logic.user.service.UserRegistrationService;
+import ch.zhaw.rateit.api.logic.user.entity.User;
+import ch.zhaw.rateit.api.logic.user.entity.UserRegistrationRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Controller to handle interactions with users.
+ *
+ * @author Nicolas Zillig
+ */
+@RestController
+public class UserController {
+
+    private final UserRegistrationService userRegistrationService;
+
+    @Autowired
+    public UserController(UserRegistrationService userRegistrationService) {
+        this.userRegistrationService = userRegistrationService;
+    }
+
+    @PostMapping(path = "/register")
+    public User register(@RequestBody UserRegistrationRequest userRegistrationRequest) {
+        return userRegistrationService.register(userRegistrationRequest);
+    }
+}
diff --git a/api/src/main/java/ch/zhaw/rateit/api/logic/user/entity/User.java b/api/src/main/java/ch/zhaw/rateit/api/logic/user/entity/User.java
new file mode 100644
index 00000000..1aec08b9
--- /dev/null
+++ b/api/src/main/java/ch/zhaw/rateit/api/logic/user/entity/User.java
@@ -0,0 +1,48 @@
+package ch.zhaw.rateit.api.logic.user.entity;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+/**
+ * Dataclass for users in database.
+ *
+ * @author Nicolas Zillig
+ */
+@Document
+public class User {
+    @Id
+    private String id;
+    private String email;
+    private String displayName;
+    private String hashedPassword;
+
+    public User(String email, String displayName, String hashedPassword) {
+        this.email = email;
+        this.displayName = displayName;
+        this.hashedPassword = hashedPassword;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public void setDisplayName(String displayName) {
+        this.displayName = displayName;
+    }
+
+    public String getHashedPassword() {
+        return hashedPassword;
+    }
+
+    public void setHashedPassword(String hashedPassword) {
+        this.hashedPassword = hashedPassword;
+    }
+}
diff --git a/api/src/main/java/ch/zhaw/rateit/api/logic/user/entity/UserRegistrationRequest.java b/api/src/main/java/ch/zhaw/rateit/api/logic/user/entity/UserRegistrationRequest.java
new file mode 100644
index 00000000..185ef828
--- /dev/null
+++ b/api/src/main/java/ch/zhaw/rateit/api/logic/user/entity/UserRegistrationRequest.java
@@ -0,0 +1,43 @@
+package ch.zhaw.rateit.api.logic.user.entity;
+
+/**
+ * Dataclass to store user requests. This is used when a new user is registered.
+ * The datafields of this class are used by other controller and service classes to calculate values for user objects.
+ *
+ * @author Nicolas Zillig
+ */
+public class UserRegistrationRequest {
+    private String email;
+    private String displayName;
+    private String cleanPassword;
+
+    public UserRegistrationRequest(String email, String displayName, String cleanPassword) {
+        this.email = email;
+        this.displayName = displayName;
+        this.cleanPassword = cleanPassword;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public void setDisplayName(String displayName) {
+        this.displayName = displayName;
+    }
+
+    public String getCleanPassword() {
+        return cleanPassword;
+    }
+
+    public void setCleanPassword(String cleanPassword) {
+        this.cleanPassword = cleanPassword;
+    }
+}
diff --git a/api/src/main/java/ch/zhaw/rateit/api/logic/user/repository/UserRepository.java b/api/src/main/java/ch/zhaw/rateit/api/logic/user/repository/UserRepository.java
new file mode 100644
index 00000000..591915d0
--- /dev/null
+++ b/api/src/main/java/ch/zhaw/rateit/api/logic/user/repository/UserRepository.java
@@ -0,0 +1,15 @@
+package ch.zhaw.rateit.api.logic.user.repository;
+
+import ch.zhaw.rateit.api.logic.user.entity.User;
+import org.springframework.data.mongodb.repository.MongoRepository;
+
+import java.util.Optional;
+
+/**
+ * Repository for users.
+ *
+ * @author Nicolas Zillig
+ */
+public interface UserRepository extends MongoRepository<User, String> {
+    Optional<User> findByEmail(String email);
+}
diff --git a/api/src/main/java/ch/zhaw/rateit/api/logic/user/service/UserRegistrationService.java b/api/src/main/java/ch/zhaw/rateit/api/logic/user/service/UserRegistrationService.java
new file mode 100644
index 00000000..0fd55fd8
--- /dev/null
+++ b/api/src/main/java/ch/zhaw/rateit/api/logic/user/service/UserRegistrationService.java
@@ -0,0 +1,42 @@
+package ch.zhaw.rateit.api.logic.user.service;
+
+import ch.zhaw.rateit.api.exceptions.types.DuplicateEmailUserException;
+import ch.zhaw.rateit.api.logic.user.entity.User;
+import ch.zhaw.rateit.api.logic.user.entity.UserRegistrationRequest;
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+/**
+ * Class to handle user creation.
+ *
+ * @author Nicolas Zillig
+ */
+@Service
+public class UserRegistrationService {
+    private final UserRepository userRepository;
+    private final PasswordEncoder passwordEncrypter;
+
+    public UserRegistrationService(UserRepository userRepository, PasswordEncoder passwordEncrypter) {
+        this.userRepository = userRepository;
+        this.passwordEncrypter = passwordEncrypter;
+    }
+
+    public User register(UserRegistrationRequest userRegistrationRequest) {
+        checkEmailUnique(userRegistrationRequest.getEmail());
+
+        User newUser = new User(userRegistrationRequest.getEmail(), userRegistrationRequest.getDisplayName(), hashCleanPassword(userRegistrationRequest.getCleanPassword()));
+        userRepository.save(newUser);
+        return newUser;
+    }
+
+    private void checkEmailUnique(String email) {
+        userRepository.findByEmail(email).ifPresent(user -> {
+            throw new DuplicateEmailUserException("User with email " + email + " already exists");
+        });
+    }
+
+    private String hashCleanPassword(String password) {
+        return passwordEncrypter.encode(password);
+    }
+}
diff --git a/api/src/main/resources/application-dev.properties b/api/src/main/resources/application-dev.properties
index c4316c2f..3d395b66 100644
--- a/api/src/main/resources/application-dev.properties
+++ b/api/src/main/resources/application-dev.properties
@@ -1,5 +1,6 @@
 cors.allowed-origins=http://localhost:4200
 
+
 spring.data.mongodb.host=localhost
 spring.data.mongodb.port=27017
 spring.data.mongodb.database=rateit
diff --git a/api/src/test/java/ch/zhaw/rateit/api/RateitAPIRequestTest.java b/api/src/test/java/ch/zhaw/rateit/api/RateitAPIRequestTest.java
index b797a099..5e1946d6 100644
--- a/api/src/test/java/ch/zhaw/rateit/api/RateitAPIRequestTest.java
+++ b/api/src/test/java/ch/zhaw/rateit/api/RateitAPIRequestTest.java
@@ -1,16 +1,13 @@
 package ch.zhaw.rateit.api;
 
 import ch.zhaw.rateit.api.config.WebsecurityConfig;
-import ch.zhaw.rateit.api.test.TestEntityRepository;
 import ch.zhaw.rateit.api.util.AbstractBaseIntegrationTest;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
 import org.springframework.context.annotation.Import;
 import org.springframework.test.web.servlet.MockMvc;
 
-import static org.assertj.core.api.Fail.fail;
 import static org.hamcrest.Matchers.containsString;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
diff --git a/api/src/test/java/ch/zhaw/rateit/api/RateitAPIUserRegistrationTest.java b/api/src/test/java/ch/zhaw/rateit/api/RateitAPIUserRegistrationTest.java
new file mode 100644
index 00000000..50270863
--- /dev/null
+++ b/api/src/test/java/ch/zhaw/rateit/api/RateitAPIUserRegistrationTest.java
@@ -0,0 +1,107 @@
+package ch.zhaw.rateit.api;
+
+import ch.zhaw.rateit.api.exceptions.types.DuplicateEmailUserException;
+import ch.zhaw.rateit.api.logic.user.entity.User;
+import ch.zhaw.rateit.api.logic.user.entity.UserRegistrationRequest;
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import ch.zhaw.rateit.api.config.WebsecurityConfig;
+import ch.zhaw.rateit.api.util.AbstractBaseIntegrationTest;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.MediaType;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+
+@AutoConfigureMockMvc
+@Import(WebsecurityConfig.class)
+class RateitAPIUserRegistrationTest extends AbstractBaseIntegrationTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @Autowired
+    private PasswordEncoder passwordEncrypter;
+
+    @BeforeEach
+    void cleanDatabase() {
+        userRepository.deleteAll();
+    }
+    @Test
+    void endpointRegisterPositive() throws Exception {
+        String email = "test@example.com";
+        String displayName = "TestUser";
+        String cleanPassword = "mostSecurePassword";
+        UserRegistrationRequest userRegistrationRequest = new UserRegistrationRequest(email, displayName, cleanPassword);
+        String requestBody = objectMapper.writeValueAsString(userRegistrationRequest);
+
+        String result = mockMvc.perform(post("/register")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(requestBody))
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        User resultObject = objectMapper.readValue(result, User.class);
+        assertNotNull(resultObject);
+        assertEquals(email, resultObject.getEmail());
+        assertEquals(displayName, resultObject.getDisplayName());
+        assertTrue(passwordEncrypter.matches(cleanPassword, resultObject.getHashedPassword()));
+    }
+
+    @Test
+    void endpointRegisterPositiveDisplaynameAlreadyExists() throws Exception {
+        String displayName = "TestUser";
+        String email2 = "test2@example.com";
+        String cleanPassword = "mostSecurePassword2";
+        UserRegistrationRequest userRegistrationRequest = new UserRegistrationRequest(email2, displayName, cleanPassword);
+        String requestBody = objectMapper.writeValueAsString(userRegistrationRequest);
+
+        User user = new User("test1@example.com", "TestUser1", "mostSecurePassword1");
+        userRepository.save(user);
+
+        String result = mockMvc.perform(post("/register")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(requestBody))
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        User resultObject = objectMapper.readValue(result, User.class);
+        assertNotNull(resultObject);
+        assertEquals(email2, resultObject.getEmail());
+        assertEquals(displayName, resultObject.getDisplayName());
+        assertTrue(passwordEncrypter.matches(cleanPassword, resultObject.getHashedPassword()));
+    }
+
+    @Test
+    void endpointRegisterNegativeEmailAlreadyExisting() throws Exception {
+        String email = "test@example.com";
+        UserRegistrationRequest userRegistrationRequest = new UserRegistrationRequest(email, "TestUser2", "mostSecurePassword2");
+        String requestBody = objectMapper.writeValueAsString(userRegistrationRequest);
+
+        User user = new User(email, "TestUser1", "mostSecurePassword1");
+        userRepository.save(user);
+
+        Exception resolvedException = mockMvc.perform(post("/register")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(requestBody))
+                .andReturn().getResolvedException();
+
+        assertNotNull(resolvedException);
+        assertInstanceOf(DuplicateEmailUserException.class, resolvedException);
+        assertTrue(resolvedException.getMessage().contains("User with email " + email + " already exists"));
+    }
+}


Reviews:
user: login: averel10
body: So has reviewed, grundsätzlich isch wichtigste da het eifach no paar sache wo mer mache mues. Han au grad die Sache integriert wo am Sonarcloud nöd gfalled. Es het denn det amigs witeri infos wieso gwüssi sache nöd so gschied sind und wie mers chan löse.

Ich han i mim review no au Validierig vo UserInputs inngenoh, chönt mer aber au nachträglich mache => i dem Fall mir Bscheid gäh und es separates issue dir assigne.

Und ha no de aktuelli stage innegmergt denn gsesch au z.b bi de tests was mer cha mache und so.

Bi Frage/Unklarheite gern melde.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-23T16:04:19Z
user: login: zillinic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-23T18:22:22Z
user: login: zillinic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T17:11:33Z
user: login: zillinic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T17:11:46Z
user: login: zillinic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T17:13:15Z
user: login: zillinic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T17:18:16Z
user: login: zillinic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T18:20:06Z
user: login: zillinic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T18:20:20Z
user: login: zillinic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T18:22:54Z
user: login: zillinic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T23:13:11Z
user: login: zillinic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T23:20:05Z
user: login: zillinic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T16:25:47Z
user: login: zillinic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T16:25:59Z
user: login: zillinic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T16:26:27Z
user: login: averel10
body: approved and merged => coverage is only an issue because of pojo's (which should not be tested).
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-25T19:42:35Z

Review comments:
diff_hunk: @@ -20,6 +21,8 @@ public class WebsecurityConfig {
     public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
         return http
                 .authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().permitAll())
+                //ToDo remove csrf again
+                .csrf(AbstractHttpConfigurer::disable)
path: api/src/main/java/ch/zhaw/rateit/api/config/WebsecurityConfig.java
user: login: averel10
body: Das Deaktivieren von CSRF ist hier zulässig, da wir eine REST-API haben, die stateless ist, somit kann das TODO entfernt werden. Zusätzlich muss in Sonar das Security-Issue aktzeptiert werden.
Weitere Infos: https://www.baeldung.com/csrf-stateless-rest-api (Wir wären Kapitel 2.2)
created_at: 2025-03-23T15:11:29Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,26 @@
+package ch.zhaw.rateit.api.logic.user.Controller;
+
+import ch.zhaw.rateit.api.logic.user.UserService;
+import ch.zhaw.rateit.api.logic.user.entity.User;
+import ch.zhaw.rateit.api.logic.user.entity.UserRegistrationRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+
+@RestController
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/Controller/UserController.java
user: login: averel10
body: Javadoc Missing
created_at: 2025-03-23T15:12:15Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,26 @@
+package ch.zhaw.rateit.api.logic.user.Controller;
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/Controller/UserController.java
user: login: averel10
body: Package-Namen klein: Controller -> controller
created_at: 2025-03-23T15:12:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,12 @@
+package ch.zhaw.rateit.api.logic.user;
+
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.stereotype.Service;
+
+@Service
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/PasswordEncrypter.java
user: login: averel10
body: @ Configuration verwenden gemäss Klassenkommentar
created_at: 2025-03-23T15:15:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,37 @@
+package ch.zhaw.rateit.api.logic.user;
+
+import ch.zhaw.rateit.api.logic.user.entity.User;
+import ch.zhaw.rateit.api.logic.user.entity.UserRegistrationRequest;
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserService {
+    private final UserRepository userRepository;
+    private final PasswordEncrypter passwordEncrypter;
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/UserService.java
user: login: averel10
body: Hier direkt ein Bean BCryptPasswordEncoder autowiren
created_at: 2025-03-23T15:15:52Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,12 @@
+package ch.zhaw.rateit.api.logic.user;
+
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.stereotype.Service;
+
+@Service
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/PasswordEncrypter.java
user: login: averel10
body: Javadoc missing
created_at: 2025-03-23T15:16:44Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,37 @@
+package ch.zhaw.rateit.api.logic.user;
+
+import ch.zhaw.rateit.api.logic.user.entity.User;
+import ch.zhaw.rateit.api.logic.user.entity.UserRegistrationRequest;
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserService {
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/UserService.java
user: login: averel10
body: Javadoc Missing
created_at: 2025-03-23T15:16:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,43 @@
+package ch.zhaw.rateit.api.logic.user.entity;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+@Document
+public class User {
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/entity/User.java
user: login: averel10
body: Javadoc Missing
created_at: 2025-03-23T15:17:31Z
author_association: COLLABORATOR
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/PasswordEncrypter.java
user: login: averel10
body: Der Aufbau ist gemäss SpringBoot nicht ganz korrekt: 
Anstatt dass wir hier eine Wrapper-Klasse machen (also PasswordEncrypter) kann hier direkt ein Bean(Instanz) von BCryptPasswordEncrypter in den SpringBootContext geladen werden.
created_at: 2025-03-23T15:18:09Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,12 @@
+package ch.zhaw.rateit.api.logic.user;
+
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.stereotype.Service;
+
+@Service
+public class PasswordEncrypter {
+    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+    public BCryptPasswordEncoder getPasswordEncoder() {
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/PasswordEncrypter.java
user: login: averel10
body: @ Bean verwenden gemäss Klassenkommentar
created_at: 2025-03-23T15:18:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,37 @@
+package ch.zhaw.rateit.api.logic.user.entity;
+
+public class UserRegistrationRequest {
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/entity/UserRegistrationRequest.java
user: login: averel10
body: Javadoc missing
created_at: 2025-03-23T15:20:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,10 @@
+package ch.zhaw.rateit.api.logic.user.repository;
+
+import ch.zhaw.rateit.api.logic.user.entity.User;
+import org.springframework.data.mongodb.repository.MongoRepository;
+
+import java.util.Optional;
+
+public interface UserRepository extends MongoRepository<User, String> {
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/repository/UserRepository.java
user: login: averel10
body: Javadoc missing
created_at: 2025-03-23T15:20:29Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,117 @@
+package ch.zhaw.rateit.api;
+
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import ch.zhaw.rateit.api.config.WebsecurityConfig;
+import ch.zhaw.rateit.api.util.AbstractBaseIntegrationTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@AutoConfigureMockMvc
+@Import(WebsecurityConfig.class)
+public class RateitAPIUserTest extends AbstractBaseIntegrationTest {
path: api/src/test/java/ch/zhaw/rateit/api/RateitAPIUserTest.java
user: login: averel10
body: Benenung etwas ungünstig eher sowas wie RateitAPIUserRegistrationTest (ist ja das einzige was getestet wird)
created_at: 2025-03-23T15:22:43Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,117 @@
+package ch.zhaw.rateit.api;
+
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import ch.zhaw.rateit.api.config.WebsecurityConfig;
+import ch.zhaw.rateit.api.util.AbstractBaseIntegrationTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@AutoConfigureMockMvc
+@Import(WebsecurityConfig.class)
+public class RateitAPIUserTest extends AbstractBaseIntegrationTest {
path: api/src/test/java/ch/zhaw/rateit/api/RateitAPIUserTest.java
user: login: averel10
body: Und Test-Klassen sollten immer package-private sein: also public weglassen
created_at: 2025-03-23T15:25:17Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,37 @@
+package ch.zhaw.rateit.api.logic.user;
+
+import ch.zhaw.rateit.api.logic.user.entity.User;
+import ch.zhaw.rateit.api.logic.user.entity.UserRegistrationRequest;
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserService {
+    private final UserRepository userRepository;
+    private final PasswordEncrypter passwordEncrypter;
+
+    public UserService(UserRepository userRepository, PasswordEncrypter passwordEncrypter) {
+        this.userRepository = userRepository;
+        this.passwordEncrypter = passwordEncrypter;
+    }
+
+    public User register(UserRegistrationRequest userRegistrationRequest) {
+        checkEmailUnique(userRegistrationRequest.getEmail());
+
+        User newUser = new User(userRegistrationRequest.getEmail(), userRegistrationRequest.getDisplayName(), hashCleanPassword(userRegistrationRequest.getCleanPassword()));
+        userRepository.save(newUser);
+        return newUser;
+    }
+
+    private void checkEmailUnique(String email) {
+        userRepository.findByEmail(email).ifPresent(user -> {
+            throw new RuntimeException("User with email " + email + " already exists");
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/UserService.java
user: login: averel10
body: Hier müsste eine spezifischere Exception geworfen werden. Am besten eine neue Exception in separater Klasse definieren, die RuntimeException extended: z.B. DuplicateEmailUserException.


created_at: 2025-03-23T15:37:25Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,37 @@
+package ch.zhaw.rateit.api.logic.user;
+
+import ch.zhaw.rateit.api.logic.user.entity.User;
+import ch.zhaw.rateit.api.logic.user.entity.UserRegistrationRequest;
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserService {
+    private final UserRepository userRepository;
+    private final PasswordEncrypter passwordEncrypter;
+
+    public UserService(UserRepository userRepository, PasswordEncrypter passwordEncrypter) {
+        this.userRepository = userRepository;
+        this.passwordEncrypter = passwordEncrypter;
+    }
+
+    public User register(UserRegistrationRequest userRegistrationRequest) {
+        checkEmailUnique(userRegistrationRequest.getEmail());
+
+        User newUser = new User(userRegistrationRequest.getEmail(), userRegistrationRequest.getDisplayName(), hashCleanPassword(userRegistrationRequest.getCleanPassword()));
+        userRepository.save(newUser);
+        return newUser;
+    }
+
+    private void checkEmailUnique(String email) {
+        userRepository.findByEmail(email).ifPresent(user -> {
+            throw new RuntimeException("User with email " + email + " already exists");
+        });
+    }
+
+    private String hashCleanPassword(String password) {
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/UserService.java
user: login: averel10
body: Sieht korrekt aus. Bcrypt produziert bei gleichem Input verschiedene Hashes: 
![image](https://github.com/user-attachments/assets/2cb5f33b-1a4a-4482-8aa6-51b50c294c86)
created_at: 2025-03-23T15:38:15Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,117 @@
+package ch.zhaw.rateit.api;
+
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import ch.zhaw.rateit.api.config.WebsecurityConfig;
+import ch.zhaw.rateit.api.util.AbstractBaseIntegrationTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@AutoConfigureMockMvc
+@Import(WebsecurityConfig.class)
+public class RateitAPIUserTest extends AbstractBaseIntegrationTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @BeforeEach
+    void cleanDatabase() {
+        userRepository.deleteAll();
+    }
+    @Test
+    void endpointRegisterPositive() throws Exception {
+        String email = "test@example.com";
+        String displayName = "TestUser";
+
+        mockMvc.perform(post("/register")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("""
path: api/src/test/java/ch/zhaw/rateit/api/RateitAPIUserTest.java
user: login: averel10
body: Ich würde hier nicht direkt mit JSON-Strings arbeiten: lieber eine Instanz des UserRegistrationRequests erstellen und konfigurieren und dann mit ObjectMapper zu einem String verwandeln:
objectMapper.writeValueAsString(request)

ObjectMapper kann einfach mittels @ Autowired geladen werden, am Anfang der Klasse:
    @ Autowired
    private ObjectMapper objectMapper;

Ist längerfristig wartbarer und dynamischer.

Gilt analog für die anderen Tests.
created_at: 2025-03-23T15:50:05Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,117 @@
+package ch.zhaw.rateit.api;
+
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import ch.zhaw.rateit.api.config.WebsecurityConfig;
+import ch.zhaw.rateit.api.util.AbstractBaseIntegrationTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@AutoConfigureMockMvc
+@Import(WebsecurityConfig.class)
+public class RateitAPIUserTest extends AbstractBaseIntegrationTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @BeforeEach
+    void cleanDatabase() {
+        userRepository.deleteAll();
+    }
+    @Test
+    void endpointRegisterPositive() throws Exception {
+        String email = "test@example.com";
+        String displayName = "TestUser";
+
+        mockMvc.perform(post("/register")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("""
+                        {
+                            "email": "%s",
+                            "displayName": "%s",
+                            "cleanPassword": "mostSecurePassword"
+                        }
+                        """.formatted(email, displayName)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.email").value(email))
path: api/src/test/java/ch/zhaw/rateit/api/RateitAPIUserTest.java
user: login: averel10
body: Json-Vergleiche in dieser Form sind etwas doof zum lesen. Hier würde man typischerweise die Antwort wieder mittels ObjectMapper auf die Entitäts-Klasse zurückkonvertieren und dann mit dem typisierten Objekt testen:

![image](https://github.com/user-attachments/assets/6fa085c4-993b-4c51-9cb5-0f2ef2548d36)

Gilt analog für andere Tests

created_at: 2025-03-23T15:51:55Z
author_association: COLLABORATOR
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/entity/UserRegistrationRequest.java
user: login: averel10
body: Grundsätzlich fehlt hier noch die Validierung der Felder anhand JSR-Annotationen: https://www.baeldung.com/spring-boot-bean-validation

=> ergibt dann auch neue Tests mit fehlenden Daten, falschen Daten
created_at: 2025-03-23T15:55:19Z
author_association: COLLABORATOR
path: api/src/test/java/ch/zhaw/rateit/api/RateitAPIUserTest.java
user: login: averel10
body: Grundsätzlich ist das Testing ok.
Folgende Dinge wären noch von Vorteil:
- Positivtest: Nach erfolgreicher Registrierung überprüfen ob der Hash mit dem Passwort effektiv validiert werden kann (siehe Methode bcryptPasswordEncoder.matches um ein Passwort gegen ein Hash zu überprüfen)
created_at: 2025-03-23T16:01:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,37 @@
+package ch.zhaw.rateit.api.logic.user;
+
+import ch.zhaw.rateit.api.logic.user.entity.User;
+import ch.zhaw.rateit.api.logic.user.entity.UserRegistrationRequest;
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserService {
+    private final UserRepository userRepository;
+    private final PasswordEncrypter passwordEncrypter;
+
+    public UserService(UserRepository userRepository, PasswordEncrypter passwordEncrypter) {
+        this.userRepository = userRepository;
+        this.passwordEncrypter = passwordEncrypter;
+    }
+
+    public User register(UserRegistrationRequest userRegistrationRequest) {
+        checkEmailUnique(userRegistrationRequest.getEmail());
+
+        User newUser = new User(userRegistrationRequest.getEmail(), userRegistrationRequest.getDisplayName(), hashCleanPassword(userRegistrationRequest.getCleanPassword()));
+        userRepository.save(newUser);
+        return newUser;
+    }
+
+    private void checkEmailUnique(String email) {
+        userRepository.findByEmail(email).ifPresent(user -> {
+            throw new RuntimeException("User with email " + email + " already exists");
+        });
+    }
+
+    private String hashCleanPassword(String password) {
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/UserService.java
user: login: averel10
body: Ein Test wäre aber sicher nicht schlecht => siehe Kommentar Test-Klasse
created_at: 2025-03-23T16:02:36Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,26 @@
+package ch.zhaw.rateit.api.logic.user.Controller;
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/Controller/UserController.java
user: login: zillinic
body: Habe ich umbenannt, zusätzlich noch UserService in eigenes package verschoben.
created_at: 2025-03-23T18:22:22Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,26 @@
+package ch.zhaw.rateit.api.logic.user.Controller;
+
+import ch.zhaw.rateit.api.logic.user.UserService;
+import ch.zhaw.rateit.api.logic.user.entity.User;
+import ch.zhaw.rateit.api.logic.user.entity.UserRegistrationRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+
+@RestController
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/Controller/UserController.java
user: login: zillinic
body: done
created_at: 2025-03-24T17:11:33Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,12 @@
+package ch.zhaw.rateit.api.logic.user;
+
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.stereotype.Service;
+
+@Service
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/PasswordEncrypter.java
user: login: zillinic
body: hinzugefügt
created_at: 2025-03-24T17:11:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,12 @@
+package ch.zhaw.rateit.api.logic.user;
+
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.stereotype.Service;
+
+@Service
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/PasswordEncrypter.java
user: login: zillinic
body: added
created_at: 2025-03-24T17:13:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,37 @@
+package ch.zhaw.rateit.api.logic.user;
+
+import ch.zhaw.rateit.api.logic.user.entity.User;
+import ch.zhaw.rateit.api.logic.user.entity.UserRegistrationRequest;
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserService {
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/UserService.java
user: login: zillinic
body: added
created_at: 2025-03-24T17:18:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,43 @@
+package ch.zhaw.rateit.api.logic.user.entity;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+@Document
+public class User {
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/entity/User.java
user: login: zillinic
body: added
created_at: 2025-03-24T18:20:06Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,37 @@
+package ch.zhaw.rateit.api.logic.user.entity;
+
+public class UserRegistrationRequest {
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/entity/UserRegistrationRequest.java
user: login: zillinic
body: added
created_at: 2025-03-24T18:20:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,10 @@
+package ch.zhaw.rateit.api.logic.user.repository;
+
+import ch.zhaw.rateit.api.logic.user.entity.User;
+import org.springframework.data.mongodb.repository.MongoRepository;
+
+import java.util.Optional;
+
+public interface UserRepository extends MongoRepository<User, String> {
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/repository/UserRepository.java
user: login: zillinic
body: added
created_at: 2025-03-24T18:22:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,37 @@
+package ch.zhaw.rateit.api.logic.user;
+
+import ch.zhaw.rateit.api.logic.user.entity.User;
+import ch.zhaw.rateit.api.logic.user.entity.UserRegistrationRequest;
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserService {
+    private final UserRepository userRepository;
+    private final PasswordEncrypter passwordEncrypter;
+
+    public UserService(UserRepository userRepository, PasswordEncrypter passwordEncrypter) {
+        this.userRepository = userRepository;
+        this.passwordEncrypter = passwordEncrypter;
+    }
+
+    public User register(UserRegistrationRequest userRegistrationRequest) {
+        checkEmailUnique(userRegistrationRequest.getEmail());
+
+        User newUser = new User(userRegistrationRequest.getEmail(), userRegistrationRequest.getDisplayName(), hashCleanPassword(userRegistrationRequest.getCleanPassword()));
+        userRepository.save(newUser);
+        return newUser;
+    }
+
+    private void checkEmailUnique(String email) {
+        userRepository.findByEmail(email).ifPresent(user -> {
+            throw new RuntimeException("User with email " + email + " already exists");
path: api/src/main/java/ch/zhaw/rateit/api/logic/user/UserService.java
user: login: zillinic
body: implementiert inklusive Handler
created_at: 2025-03-24T23:13:11Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,117 @@
+package ch.zhaw.rateit.api;
+
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import ch.zhaw.rateit.api.config.WebsecurityConfig;
+import ch.zhaw.rateit.api.util.AbstractBaseIntegrationTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@AutoConfigureMockMvc
+@Import(WebsecurityConfig.class)
+public class RateitAPIUserTest extends AbstractBaseIntegrationTest {
path: api/src/test/java/ch/zhaw/rateit/api/RateitAPIUserTest.java
user: login: zillinic
body: Testklasse ist nun umbenannt und package-private
created_at: 2025-03-24T23:20:05Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,117 @@
+package ch.zhaw.rateit.api;
+
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import ch.zhaw.rateit.api.config.WebsecurityConfig;
+import ch.zhaw.rateit.api.util.AbstractBaseIntegrationTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@AutoConfigureMockMvc
+@Import(WebsecurityConfig.class)
+public class RateitAPIUserTest extends AbstractBaseIntegrationTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @BeforeEach
+    void cleanDatabase() {
+        userRepository.deleteAll();
+    }
+    @Test
+    void endpointRegisterPositive() throws Exception {
+        String email = "test@example.com";
+        String displayName = "TestUser";
+
+        mockMvc.perform(post("/register")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("""
path: api/src/test/java/ch/zhaw/rateit/api/RateitAPIUserTest.java
user: login: zillinic
body: Objectmapper ist verwendet, Stringbodys sind ersetzt
created_at: 2025-03-25T16:25:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,117 @@
+package ch.zhaw.rateit.api;
+
+import ch.zhaw.rateit.api.logic.user.repository.UserRepository;
+import ch.zhaw.rateit.api.config.WebsecurityConfig;
+import ch.zhaw.rateit.api.util.AbstractBaseIntegrationTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@AutoConfigureMockMvc
+@Import(WebsecurityConfig.class)
+public class RateitAPIUserTest extends AbstractBaseIntegrationTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @BeforeEach
+    void cleanDatabase() {
+        userRepository.deleteAll();
+    }
+    @Test
+    void endpointRegisterPositive() throws Exception {
+        String email = "test@example.com";
+        String displayName = "TestUser";
+
+        mockMvc.perform(post("/register")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("""
+                        {
+                            "email": "%s",
+                            "displayName": "%s",
+                            "cleanPassword": "mostSecurePassword"
+                        }
+                        """.formatted(email, displayName)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.email").value(email))
path: api/src/test/java/ch/zhaw/rateit/api/RateitAPIUserTest.java
user: login: zillinic
body: erledigt
created_at: 2025-03-25T16:25:59Z
author_association: COLLABORATOR
path: api/src/test/java/ch/zhaw/rateit/api/RateitAPIUserTest.java
user: login: zillinic
body: Passworthash wird bei positivtests nun geprüft
created_at: 2025-03-25T16:26:27Z
author_association: COLLABORATOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-25T19:43:33Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=rateit_infrastructure&pullRequest=158) **Quality Gate passed for 'Rateit Infrastructure'**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=rateit_infrastructure&pullRequest=158&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=rateit_infrastructure&pullRequest=158&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=rateit_infrastructure&pullRequest=158&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=rateit_infrastructure&pullRequest=158&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=rateit_infrastructure&pullRequest=158&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=rateit_infrastructure&pullRequest=158)


author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-03-25T19:44:10Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=rateit-pm4-org_rateit_api&pullRequest=158) **Quality Gate failed for 'Rateit API'**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [78.8% Coverage on New Code](https://sonarcloud.io/component_measures?id=rateit-pm4-org_rateit_api&pullRequest=158&metric=new_coverage&view=list) (required ≥ 80%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=rateit-pm4-org_rateit_api&pullRequest=158)


author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-03-25T19:44:16Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=rateit-pm4-org_rateit-frontend&pullRequest=158) **Quality Gate passed for 'Rateit Frontend'**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=rateit-pm4-org_rateit-frontend&pullRequest=158&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=rateit-pm4-org_rateit-frontend&pullRequest=158&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=rateit-pm4-org_rateit-frontend&pullRequest=158&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=rateit-pm4-org_rateit-frontend&pullRequest=158&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=rateit-pm4-org_rateit-frontend&pullRequest=158&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=rateit-pm4-org_rateit-frontend&pullRequest=158)


author_association: NONE