Repository: VictorPBilbao/ProjetoWeb2
Pull request #77: Implement user registration with validation and error handling
Author: login: VictorPBilbao
Created: 2025-04-20T20:45:44Z | Merged: 2025-04-20T20:48:18Z

Introduce a user registration endpoint with comprehensive validation for user details, including first name, last name, CPF, date of birth, and address. Enhance the authentication service to validate usernames and emails, and improve user retrieval with debug logging. Add necessary data models and repositories to support user registration functionality.

## Summary by Sourcery

Implement comprehensive user registration system with robust validation, error handling, and detailed user information capture

New Features:
- Add user registration endpoint with detailed personal and address information
- Implement username and email availability validation endpoints
- Create comprehensive validation for user registration data

Bug Fixes:
- Improve user retrieval method with more robust username and email lookup

Enhancements:
- Improve authentication service with more detailed error handling
- Enhance user repository with additional query methods
- Add global exception handling for validation errors

Build:
- Add Jakarta validation and Hibernate validator dependencies

Tests:
- Add validation constraints for user registration fields


Diff:
diff --git a/byteassist-backend/pom.xml b/byteassist-backend/pom.xml
index 356c8bb..1c78ad4 100644
--- a/byteassist-backend/pom.xml
+++ b/byteassist-backend/pom.xml
@@ -82,6 +82,19 @@
             <artifactId>spring-security-crypto</artifactId>
             <version>6.1.2</version> <!-- Use the latest version -->
         </dependency>
+
+        <dependency>
+            <groupId>jakarta.validation</groupId>
+            <artifactId>jakarta.validation-api</artifactId>
+            <version>3.0.2</version>
+        </dependency>
+
+        <!-- Add Hibernate Validator - implementation of Jakarta Bean Validation -->
+        <dependency>
+            <groupId>org.hibernate.validator</groupId>
+            <artifactId>hibernate-validator</artifactId>
+            <version>8.0.1.Final</version>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/controller/AuthController.java b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/controller/AuthController.java
index 49ac90a..8869a19 100644
--- a/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/controller/AuthController.java
+++ b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/controller/AuthController.java
@@ -1,10 +1,17 @@
 package com.ufpr.byteassist_backend.controller;
 
+import com.ufpr.byteassist_backend.dto.RegistrationRequestDTO;
+import com.ufpr.byteassist_backend.exception.ErrorResponse;
 import com.ufpr.byteassist_backend.service.AuthService;
 
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
+import org.springframework.validation.BindingResult;
 import org.springframework.web.bind.annotation.*;
 
+import jakarta.validation.Valid;
+
 @RestController
 @RequestMapping("/auth")
 public class AuthController {
@@ -24,4 +31,21 @@ public ResponseEntity<Object> login(@RequestParam String username, @RequestParam
         System.out.println("Login request received with username: " + username + " and password: " + password);
         return authService.login(username, password);
     }
+
+    @PostMapping(value = "/register", consumes = { MediaType.APPLICATION_FORM_URLENCODED_VALUE })
+    public ResponseEntity<Object> registerForm(@Valid @ModelAttribute RegistrationRequestDTO registrationRequest,
+            BindingResult bindingResult) {
+        System.out.println("Form registration request received: " + registrationRequest);
+        return authService.register(registrationRequest, bindingResult);
+    }
+
+    @GetMapping("/validate/username/{username}")
+    public ResponseEntity<Object> validateUsername(@PathVariable String username) {
+        return authService.validateUsername(username);
+    }
+
+    @GetMapping("/validate/email/{email}")
+    public ResponseEntity<Object> validateEmail(@PathVariable String email) {
+        return authService.validateEmail(email);
+    }
 }
\ No newline at end of file
diff --git a/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/dto/RegistrationRequestDTO.java b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/dto/RegistrationRequestDTO.java
new file mode 100644
index 0000000..0fc7249
--- /dev/null
+++ b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/dto/RegistrationRequestDTO.java
@@ -0,0 +1,72 @@
+package com.ufpr.byteassist_backend.dto;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Data
+public class RegistrationRequestDTO {
+
+    @NotBlank(message = "First name is required")
+    @Size(max = 50, message = "First name must not exceed 50 characters")
+    private String firstName;
+
+    @NotBlank(message = "Last name is required")
+    @Size(max = 50, message = "Last name must not exceed 50 characters")
+    private String lastName;
+
+    @NotBlank(message = "CPF is required")
+    @Pattern(regexp = "^\\d{3}\\.\\d{3}\\.\\d{3}-\\d{2}$", message = "CPF must be in the format XXX.XXX.XXX-XX")
+    private String cpf;
+
+    @NotBlank(message = "Date of birth is required")
+    @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "Date of birth must be in the format YYYY-MM-DD")
+    private String dob;
+
+    @Size(max = 100, message = "Complemento must not exceed 100 characters")
+    private String complemento;
+
+    @NotBlank(message = "Username is required")
+    @Pattern(regexp = "^[a-z0-9._-]+$", message = "Username must be lowercase and can only contain letters, numbers, dots, underscores, and hyphens")
+    @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
+    private String username;
+
+    @NotBlank(message = "Password is required")
+    @Size(min = 8, message = "Password must be at least 8 characters")
+    private String password;
+
+    @NotBlank(message = "Email is required")
+    @Email(message = "Must be a valid email address")
+    private String email;
+
+    @NotBlank(message = "Zip code is required")
+    @Pattern(regexp = "^\\d{5}-\\d{3}$", message = "Zip code must be in the format XXXXX-XXX")
+    private String zip;
+
+    @NotBlank(message = "Street is required")
+    private String street;
+
+    @NotBlank(message = "Number is required")
+    @Size(max = 10, message = "Number must not exceed 10 characters")
+    private String number;
+
+    @NotBlank(message = "Neighborhood is required")
+    private String neighborhood;
+
+    @NotBlank(message = "City is required")
+    private String city;
+
+    @NotBlank(message = "State is required")
+    @Pattern(regexp = "^[A-Z]{2}$", message = "State must be exactly 2 uppercase letters")
+    private String state;
+
+    @NotBlank(message = "Country is required")
+    @Pattern(regexp = "^[A-Z]{2}$", message = "Country must be exactly 2 uppercase letters")
+    private String country;
+
+    @NotBlank(message = "Gender is required")
+    @Pattern(regexp = "^(Male|Female|Other)$", message = "Gender must be 'Male', 'Female', or 'Other'")
+    private String gender;
+}
\ No newline at end of file
diff --git a/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/exception/ErrorResponse.java b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/exception/ErrorResponse.java
index e3e4689..6dff135 100644
--- a/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/exception/ErrorResponse.java
+++ b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/exception/ErrorResponse.java
@@ -4,6 +4,7 @@
 
 import lombok.Builder;
 import lombok.Data;
+import java.util.Map;
 
 @Data
 @Builder
@@ -12,4 +13,5 @@ public class ErrorResponse {
     private int statusCode;
     private HttpStatus errorCode;
     private String errorDescription;
+    private Map<String, String> validationErrors;
 }
\ No newline at end of file
diff --git a/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/exception/GlobalExceptionHandler.java b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..b043652
--- /dev/null
+++ b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/exception/GlobalExceptionHandler.java
@@ -0,0 +1,86 @@
+package com.ufpr.byteassist_backend.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.validation.BindException;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@ControllerAdvice
+public class GlobalExceptionHandler {
+
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public ResponseEntity<ErrorResponse> handleValidationExceptions(MethodArgumentNotValidException ex) {
+        BindingResult bindingResult = ex.getBindingResult();
+        Map<String, String> errors = new HashMap<>();
+
+        for (FieldError error : bindingResult.getFieldErrors()) {
+            errors.put(error.getField(), error.getDefaultMessage());
+        }
+
+        ErrorResponse errorResponse = ErrorResponse.builder()
+                .message("Validation Error")
+                .statusCode(HttpStatus.BAD_REQUEST.value())
+                .errorCode(HttpStatus.BAD_REQUEST)
+                .errorDescription("The provided data is invalid")
+                .build();
+
+        // Manually set validation errors since builder may not be updated
+        errorResponse.setValidationErrors(errors);
+
+        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
+    }
+
+    @ExceptionHandler(BindException.class)
+    public ResponseEntity<ErrorResponse> handleBindExceptions(BindException ex) {
+        Map<String, String> errors = new HashMap<>();
+        ex.getBindingResult().getAllErrors().forEach((error) -> {
+            String fieldName = ((FieldError) error).getField();
+            String errorMessage = error.getDefaultMessage();
+            errors.put(fieldName, errorMessage);
+        });
+
+        ErrorResponse errorResponse = ErrorResponse.builder()
+                .message("Validation Error")
+                .statusCode(HttpStatus.BAD_REQUEST.value())
+                .errorCode(HttpStatus.BAD_REQUEST)
+                .errorDescription("The provided data contains validation errors")
+                .build();
+
+        // Manually set validation errors
+        errorResponse.setValidationErrors(errors);
+
+        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
+    }
+
+    @ExceptionHandler(HttpMessageNotReadableException.class)
+    public ResponseEntity<ErrorResponse> handleHttpMessageNotReadable(HttpMessageNotReadableException ex) {
+        ErrorResponse errorResponse = ErrorResponse.builder()
+                .message("Invalid request format")
+                .statusCode(HttpStatus.BAD_REQUEST.value())
+                .errorCode(HttpStatus.BAD_REQUEST)
+                .errorDescription("The request body could not be read. Ensure it's in the correct format.")
+                .build();
+
+        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
+    }
+
+    @ExceptionHandler(Exception.class)
+    public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex) {
+        ErrorResponse errorResponse = ErrorResponse.builder()
+                .message("Internal server error")
+                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR.value())
+                .errorCode(HttpStatus.INTERNAL_SERVER_ERROR)
+                .errorDescription("An unexpected error occurred: " + ex.getMessage())
+                .build();
+
+        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
+    }
+}
\ No newline at end of file
diff --git a/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/model/Person.java b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/model/Person.java
new file mode 100644
index 0000000..2a5e8bf
--- /dev/null
+++ b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/model/Person.java
@@ -0,0 +1,21 @@
+package com.ufpr.byteassist_backend.model;
+
+import java.time.ZonedDateTime;
+
+import com.surrealdb.RecordId;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class Person {
+    public RecordId id;
+    public String cpf;
+    public ZonedDateTime dob;
+    public String gender;
+    public PersonAddress address;
+    public PersonName name;
+}
diff --git a/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/model/PersonAddress.java b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/model/PersonAddress.java
new file mode 100644
index 0000000..edee845
--- /dev/null
+++ b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/model/PersonAddress.java
@@ -0,0 +1,18 @@
+package com.ufpr.byteassist_backend.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class PersonAddress {
+    public String zip;
+    public String number;
+    public String street;
+    public String neighborhood;
+    public String city;
+    public String state;
+    public String country;
+}
diff --git a/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/model/PersonName.java b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/model/PersonName.java
new file mode 100644
index 0000000..db2da40
--- /dev/null
+++ b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/model/PersonName.java
@@ -0,0 +1,13 @@
+package com.ufpr.byteassist_backend.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class PersonName {
+    public String first;
+    public String last;
+}
diff --git a/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/PersonRepo.java b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/PersonRepo.java
new file mode 100644
index 0000000..0e4c098
--- /dev/null
+++ b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/PersonRepo.java
@@ -0,0 +1,26 @@
+package com.ufpr.byteassist_backend.repository;
+
+import org.springframework.stereotype.Repository;
+
+import com.surrealdb.RecordId;
+import com.surrealdb.Surreal;
+import com.ufpr.byteassist_backend.model.Person;
+import com.ufpr.byteassist_backend.service.DatabaseService;
+
+@Repository
+public class PersonRepo {
+    private final Surreal db;
+
+    public PersonRepo(DatabaseService databaseService) {
+        this.db = databaseService.getDatabase();
+    }
+
+    public RecordId createPerson(Person person, String username) {
+        System.out.println("Creating person with username: " + username);
+        System.out.println("Person details: " + person);
+        Person created = db.create(Person.class, new RecordId("Person", username), person);
+        System.out.println("Person created successfully with ID: " + created.getId());
+        return created.getId();
+    }
+
+}
diff --git a/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/UserRepo.java b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/UserRepo.java
index b4955d6..6ede0fb 100644
--- a/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/UserRepo.java
+++ b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/UserRepo.java
@@ -2,6 +2,7 @@
 
 import org.springframework.stereotype.Repository;
 
+import com.surrealdb.RecordId;
 import com.surrealdb.Response;
 import com.surrealdb.Surreal;
 import com.ufpr.byteassist_backend.model.User;
@@ -17,24 +18,29 @@ public UserRepo(DatabaseService databaseService) {
     }
 
     public User getUserByUsername(String username) {
-        // TODO : Alterar para queryBind após atualização do SurrealDB
-        if (username == null || !username.matches("^[\\w@.-]+$")) {
-            // Optionally, you can log the invalid input here before returning null
-            return null;
-        }
-
-        // ? Você pode enviar mais de uma query ao mesmo tempo, separando por ponto e
-        // ? vírgula. O response é uma lista de respostas, por isso pegamos a primeira
-        // ? resposta com response.take(0) e transformamos em um array com getArray() e
-        // ? pegamos o primeiro elemento com get(0) caso necessário.
+        Response response = db
+                .query("SELECT *, id.id() AS username FROM User WHERE id.id() = '" + username + "' OR email = '"
+                        + username + "';");
         try {
-            Response response = db
-                    .query("SELECT *, id.id() AS username FROM User WHERE id.id() = '" + username + "' OR email = '"
-                            + username + "';");
             return response.take(0).getArray().get(0).get(User.class);
-        } catch (Exception e) {
+        } catch (NullPointerException e) {
             return null;
         }
     }
 
+    public boolean isUsernameAvailable(String username) {
+        Response response = db.query("SELECT * FROM User:" + username + ";");
+        return response.take(0).getArray().len() == 0;
+    }
+
+    public boolean isEmailAvailable(String email) {
+        Response response = db.query("SELECT * FROM User WHERE email = '" + email + "';");
+        return response.take(0).getArray().len() == 0;
+    }
+
+    public void createUser(User user) {
+        System.out.println("Creating user: " + user);
+        db.create(User.class, new RecordId("User", user.getUsername()), user);
+        System.out.println("User created successfully with ID: " + user.getId());
+    }
 }
diff --git a/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/service/AuthService.java b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/service/AuthService.java
index 1f000d6..40492be 100644
--- a/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/service/AuthService.java
+++ b/byteassist-backend/src/main/java/com/ufpr/byteassist_backend/service/AuthService.java
@@ -1,14 +1,30 @@
 package com.ufpr.byteassist_backend.service;
 
+import com.surrealdb.RecordId;
+import com.ufpr.byteassist_backend.dto.RegistrationRequestDTO;
 import com.ufpr.byteassist_backend.dto.UserDTO;
 import com.ufpr.byteassist_backend.exception.ErrorResponse;
+import com.ufpr.byteassist_backend.model.Person;
+import com.ufpr.byteassist_backend.model.PersonAddress;
+import com.ufpr.byteassist_backend.model.PersonName;
 import com.ufpr.byteassist_backend.model.User;
+import com.ufpr.byteassist_backend.repository.PersonRepo;
 import com.ufpr.byteassist_backend.repository.UpdateTimeRepo;
 import com.ufpr.byteassist_backend.repository.UserRepo;
+
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.stereotype.Service;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeParseException;
+import java.util.HashMap;
+import java.util.Map;
 
 @Service
 public class AuthService {
@@ -16,19 +32,23 @@ public class AuthService {
     private final UpdateTimeRepo updateTimeRepo;
     private final JwtService jwtService;
     private final BCryptPasswordEncoder passwordEncoder;
+    private final PersonRepo personRepo;
 
     // Constructor injection
-    public AuthService(UserRepo userRepo, UpdateTimeRepo updateTimeRepo, JwtService jwtService) {
+    public AuthService(UserRepo userRepo, UpdateTimeRepo updateTimeRepo, JwtService jwtService, PersonRepo personRepo) {
         this.userRepo = userRepo;
         this.updateTimeRepo = updateTimeRepo;
         this.jwtService = jwtService;
         this.passwordEncoder = new BCryptPasswordEncoder();
+        this.personRepo = personRepo;
     }
 
     public ResponseEntity<Object> login(String username, String password) {
         // print the hash of the password
         System.out.println("Password hash: " + passwordEncoder.encode(password));
         User user = userRepo.getUserByUsername(username);
+        System.out.println("User found: ");
+        System.out.println("User found: " + user);
         if (user != null && passwordEncoder.matches(password, user.getPassword())) {
             String token = jwtService.generateToken(user.getId().toString(), user.getId().toString());
             UserDTO userDTO = new UserDTO(
@@ -49,4 +69,99 @@ public ResponseEntity<Object> login(String username, String password) {
                 .build();
         return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED);
     }
+
+    public ResponseEntity<Object> register(RegistrationRequestDTO user, BindingResult bindingResult) {
+        Map<String, String> errors = new HashMap<>();
+
+        // First, process any Jakarta validation errors from the annotations in the DTO
+        if (bindingResult.hasErrors()) {
+            for (FieldError error : bindingResult.getFieldErrors()) {
+                errors.put(error.getField(), error.getDefaultMessage());
+            }
+        }
+
+        // Then, check business rules (username and email availability)
+        if (!userRepo.isUsernameAvailable(user.getUsername())) {
+            errors.put("username availability", "Username is already taken");
+        }
+
+        if (!userRepo.isEmailAvailable(user.getEmail())) {
+            errors.put("email availability", "Email is already in use");
+        }
+
+        // Validate and convert date format
+        ZonedDateTime dobZoned = null;
+        try {
+            LocalDate localDate = LocalDate.parse(user.getDob());
+            dobZoned = localDate.atStartOfDay(ZoneId.systemDefault());
+        } catch (DateTimeParseException e) {
+            errors.put("dob", "Invalid date format. Please use YYYY-MM-DD");
+        }
+
+        // If there are any errors (either validation or business rules), return them
+        if (!errors.isEmpty()) {
+            ErrorResponse errorResponse = ErrorResponse.builder()
+                    .message("Validation Error")
+                    .errorCode(HttpStatus.BAD_REQUEST)
+                    .statusCode(HttpStatus.BAD_REQUEST.value())
+                    .errorDescription("The provided registration data is invalid")
+                    .build();
+
+            // Set all validation errors
+            errorResponse.setValidationErrors(errors);
+
+            return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
+        }
+
+        // If no errors, proceed with registration
+        user.setPassword(passwordEncoder.encode(user.getPassword()));
+
+        PersonName name = new PersonName(user.getFirstName(), user.getLastName());
+        PersonAddress address = new PersonAddress(user.getZip(), user.getNumber(), user.getStreet(), user.getCity(),
+                user.getState(), user.getCountry(), user.getNeighborhood());
+        Person person = new Person(
+                null,
+                user.getCpf(),
+                dobZoned, // Using the converted ZonedDateTime
+                user.getGender(),
+                address,
+                name);
+
+        // Call createPerson method for debugging
+        RecordId createdPerson = personRepo.createPerson(person, user.getUsername());
+
+        User newUser = new User(
+                null,
+                user.getEmail(),
+                true,
+                user.getPassword(),
+                createdPerson,
+                null,
+                "Client",
+                user.getUsername());
+
+        userRepo.createUser(newUser);
+
+        return new ResponseEntity<>(HttpStatus.CREATED);
+    }
+
+    public ResponseEntity<Object> validateUsername(String username) {
+        boolean isAvailable = userRepo.isUsernameAvailable(username);
+
+        if (isAvailable) {
+            return new ResponseEntity<>(HttpStatus.OK);
+        } else {
+            return new ResponseEntity<>(HttpStatus.CONFLICT);
+        }
+    }
+
+    public ResponseEntity<Object> validateEmail(String email) {
+        boolean isAvailable = userRepo.isEmailAvailable(email);
+
+        if (isAvailable) {
+            return new ResponseEntity<>(HttpStatus.OK);
+        } else {
+            return new ResponseEntity<>(HttpStatus.CONFLICT);
+        }
+    }
 }
\ No newline at end of file


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces a user registration endpoint with comprehensive validation and error handling, enhancing the authentication service to include username/email availability checks and debug logging.  
- Added registration logic with detailed validation in AuthService and AuthController.  
- Integrated new data models and repositories (Person, PersonRepo, PersonName, PersonAddress) to support user registration.  
- Enhanced error handling through GlobalExceptionHandler and adjusted validation error responses.

### Reviewed Changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File                                                                                 | Description                                                       |
| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
| byteassist-backend/src/main/java/com/ufpr/byteassist_backend/service/AuthService.java   | Adds registration, validation logic, and new endpoints            |
| byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/UserRepo.java     | Updates user retrieval and adds username/email availability checks |
| byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/PersonRepo.java     | Implements a new repository for person data                        |
| byteassist-backend/src/main/java/com/ufpr/byteassist_backend/model/PersonName.java          | Introduces the PersonName data model                                |
| byteassist-backend/src/main/java/com/ufpr/byteassist_backend/model/PersonAddress.java       | Introduces the PersonAddress data model                             |
| byteassist-backend/src/main/java/com/ufpr/byteassist_backend/model/Person.java              | Introduces the Person data model                                    |
| byteassist-backend/src/main/java/com/ufpr/byteassist_backend/exception/GlobalExceptionHandler.java         | Enhances exception handling for validation errors                   |
| byteassist-backend/src/main/java/com/ufpr/byteassist_backend/exception/ErrorResponse.java   | Adds validationErrors field to error responses                      |
| byteassist-backend/src/main/java/com/ufpr/byteassist_backend/dto/RegistrationRequestDTO.java | Introduces a new DTO with comprehensive validation annotations        |
| byteassist-backend/src/main/java/com/ufpr/byteassist_backend/controller/AuthController.java | Adds endpoints for registration and validation checks                  |
</details>


<details>
<summary>Files not reviewed (1)</summary>

* **byteassist-backend/pom.xml**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/UserRepo.java:25**
* Catching only NullPointerException in getUserByUsername may miss other exceptions from accessing the response. Consider validating that the array has the expected elements or catching a broader exception type to safely handle unexpected cases.
```
return response.take(0).getArray().get(0).get(User.class);
```
**byteassist-backend/src/main/java/com/ufpr/byteassist_backend/service/AuthService.java:73**
* Consider adding comprehensive unit tests to cover the registration logic including validation, error handling, and successful user creation.
```
public ResponseEntity<Object> register(RegistrationRequestDTO user, BindingResult bindingResult) {
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-20T20:46:32Z
user: login: sourcery-ai[bot]
body: Hey @VictorPBilbao - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- Consider adding a dedicated exception for username/email already exists to better differentiate error scenarios.
- The validation logic in AuthService::register could be moved to a separate validator class to improve readability.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 4 issues found
- 🟡 **Security**: 2 issues found
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-20T20:46:47Z

Review comments:
diff_hunk: @@ -17,24 +18,29 @@ public UserRepo(DatabaseService databaseService) {
     }
 
     public User getUserByUsername(String username) {
-        // TODO : Alterar para queryBind após atualização do SurrealDB
-        if (username == null || !username.matches("^[\\w@.-]+$")) {
-            // Optionally, you can log the invalid input here before returning null
-            return null;
-        }
-
-        // ? Você pode enviar mais de uma query ao mesmo tempo, separando por ponto e
-        // ? vírgula. O response é uma lista de respostas, por isso pegamos a primeira
-        // ? resposta com response.take(0) e transformamos em um array com getArray() e
-        // ? pegamos o primeiro elemento com get(0) caso necessário.
+        Response response = db
+                .query("SELECT *, id.id() AS username FROM User WHERE id.id() = '" + username + "' OR email = '"
+                        + username + "';");
         try {
-            Response response = db
-                    .query("SELECT *, id.id() AS username FROM User WHERE id.id() = '" + username + "' OR email = '"
-                            + username + "';");
             return response.take(0).getArray().get(0).get(User.class);
-        } catch (Exception e) {
+        } catch (NullPointerException e) {
             return null;
         }
     }
 
+    public boolean isUsernameAvailable(String username) {
+        Response response = db.query("SELECT * FROM User:" + username + ";");
+        return response.take(0).getArray().len() == 0;
path: byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/UserRepo.java
user: login: Copilot
body: Constructing the query string via concatenation using 'username' may lead to injection or formatting issues. Consider using parameterized queries or proper input sanitization.
```suggestion
        Response response = db.query("SELECT * FROM User:$username;", Map.of("username", username));
```
created_at: 2025-04-20T20:46:31Z
author_association: NONE
diff_hunk: @@ -17,24 +18,29 @@ public UserRepo(DatabaseService databaseService) {
     }
 
     public User getUserByUsername(String username) {
-        // TODO : Alterar para queryBind após atualização do SurrealDB
-        if (username == null || !username.matches("^[\\w@.-]+$")) {
-            // Optionally, you can log the invalid input here before returning null
-            return null;
-        }
-
-        // ? Você pode enviar mais de uma query ao mesmo tempo, separando por ponto e
-        // ? vírgula. O response é uma lista de respostas, por isso pegamos a primeira
-        // ? resposta com response.take(0) e transformamos em um array com getArray() e
-        // ? pegamos o primeiro elemento com get(0) caso necessário.
+        Response response = db
+                .query("SELECT *, id.id() AS username FROM User WHERE id.id() = '" + username + "' OR email = '"
+                        + username + "';");
         try {
-            Response response = db
-                    .query("SELECT *, id.id() AS username FROM User WHERE id.id() = '" + username + "' OR email = '"
-                            + username + "';");
             return response.take(0).getArray().get(0).get(User.class);
-        } catch (Exception e) {
+        } catch (NullPointerException e) {
             return null;
         }
     }
 
+    public boolean isUsernameAvailable(String username) {
+        Response response = db.query("SELECT * FROM User:" + username + ";");
+        return response.take(0).getArray().len() == 0;
+    }
+
+    public boolean isEmailAvailable(String email) {
+        Response response = db.query("SELECT * FROM User WHERE email = '" + email + "';");
+        return response.take(0).getArray().len() == 0;
path: byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/UserRepo.java
user: login: Copilot
body: Building the query string by concatenating the 'email' parameter may expose the query to injection risks. Consider refactoring to use parameterized queries or sanitize the input accordingly.
```suggestion
        Response response = db.query("SELECT * FROM User WHERE email = $email;", Map.of("email", email));
```
created_at: 2025-04-20T20:46:32Z
author_association: NONE
diff_hunk: @@ -1,34 +1,54 @@
 package com.ufpr.byteassist_backend.service;
 
+import com.surrealdb.RecordId;
+import com.ufpr.byteassist_backend.dto.RegistrationRequestDTO;
 import com.ufpr.byteassist_backend.dto.UserDTO;
 import com.ufpr.byteassist_backend.exception.ErrorResponse;
+import com.ufpr.byteassist_backend.model.Person;
+import com.ufpr.byteassist_backend.model.PersonAddress;
+import com.ufpr.byteassist_backend.model.PersonName;
 import com.ufpr.byteassist_backend.model.User;
+import com.ufpr.byteassist_backend.repository.PersonRepo;
 import com.ufpr.byteassist_backend.repository.UpdateTimeRepo;
 import com.ufpr.byteassist_backend.repository.UserRepo;
+
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.stereotype.Service;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeParseException;
+import java.util.HashMap;
+import java.util.Map;
 
 @Service
 public class AuthService {
     private final UserRepo userRepo;
     private final UpdateTimeRepo updateTimeRepo;
     private final JwtService jwtService;
     private final BCryptPasswordEncoder passwordEncoder;
+    private final PersonRepo personRepo;
 
     // Constructor injection
-    public AuthService(UserRepo userRepo, UpdateTimeRepo updateTimeRepo, JwtService jwtService) {
+    public AuthService(UserRepo userRepo, UpdateTimeRepo updateTimeRepo, JwtService jwtService, PersonRepo personRepo) {
         this.userRepo = userRepo;
         this.updateTimeRepo = updateTimeRepo;
         this.jwtService = jwtService;
         this.passwordEncoder = new BCryptPasswordEncoder();
+        this.personRepo = personRepo;
     }
 
     public ResponseEntity<Object> login(String username, String password) {
         // print the hash of the password
         System.out.println("Password hash: " + passwordEncoder.encode(password));
         User user = userRepo.getUserByUsername(username);
+        System.out.println("User found: ");
+        System.out.println("User found: " + user);
path: byteassist-backend/src/main/java/com/ufpr/byteassist_backend/service/AuthService.java
user: login: sourcery-ai[bot]
body: **suggestion (performance):** Remove or replace debugging System.out.println statements.

Both print statements appear to be used for temporary debugging. Consider using a proper logging framework or removing them to avoid potential performance issues and extra console clutter in production.

Suggested implementation:

```java
import org.springframework.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

```

```java
public class AuthService {
    private static final Logger logger = LoggerFactory.getLogger(AuthService.class);

```

```java
        logger.debug("Password hash: {}", passwordEncoder.encode(password));

```

```java
        logger.debug("User found: {}", user);

```

Ensure that your logging configuration is set appropriately in your application's configuration so that debug messages are logged as expected. Also, remove any other instances of System.out.println used for debugging in the codebase following similar patterns.
created_at: 2025-04-20T20:46:46Z
author_association: NONE
diff_hunk: @@ -17,24 +18,29 @@ public UserRepo(DatabaseService databaseService) {
     }
 
     public User getUserByUsername(String username) {
-        // TODO : Alterar para queryBind após atualização do SurrealDB
-        if (username == null || !username.matches("^[\\w@.-]+$")) {
-            // Optionally, you can log the invalid input here before returning null
-            return null;
-        }
-
-        // ? Você pode enviar mais de uma query ao mesmo tempo, separando por ponto e
-        // ? vírgula. O response é uma lista de respostas, por isso pegamos a primeira
-        // ? resposta com response.take(0) e transformamos em um array com getArray() e
-        // ? pegamos o primeiro elemento com get(0) caso necessário.
+        Response response = db
+                .query("SELECT *, id.id() AS username FROM User WHERE id.id() = '" + username + "' OR email = '"
+                        + username + "';");
         try {
-            Response response = db
-                    .query("SELECT *, id.id() AS username FROM User WHERE id.id() = '" + username + "' OR email = '"
-                            + username + "';");
             return response.take(0).getArray().get(0).get(User.class);
-        } catch (Exception e) {
+        } catch (NullPointerException e) {
path: byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/UserRepo.java
user: login: sourcery-ai[bot]
body: **suggestion (bug_risk):** Consider broadening exception handling in getUserByUsername.

Catching only NullPointerException might overlook other runtime issues. It might be more robust to catch a broader exception type, or to handle specific exceptions more granularly to prevent silently ignoring important errors.

Suggested implementation:

```java
        } catch (Exception e) {
            // Optionally, log the exception if a logger is available
            // logger.error("Error fetching user by username", e);
            return null;

```

If your project has a logging framework set up, consider uncommenting the logger line or using the appropriate logging mechanism available in your codebase.
created_at: 2025-04-20T20:46:46Z
author_association: NONE
diff_hunk: @@ -17,24 +18,29 @@ public UserRepo(DatabaseService databaseService) {
     }
 
     public User getUserByUsername(String username) {
-        // TODO : Alterar para queryBind após atualização do SurrealDB
-        if (username == null || !username.matches("^[\\w@.-]+$")) {
-            // Optionally, you can log the invalid input here before returning null
-            return null;
-        }
-
-        // ? Você pode enviar mais de uma query ao mesmo tempo, separando por ponto e
-        // ? vírgula. O response é uma lista de respostas, por isso pegamos a primeira
-        // ? resposta com response.take(0) e transformamos em um array com getArray() e
-        // ? pegamos o primeiro elemento com get(0) caso necessário.
+        Response response = db
+                .query("SELECT *, id.id() AS username FROM User WHERE id.id() = '" + username + "' OR email = '"
+                        + username + "';");
         try {
-            Response response = db
-                    .query("SELECT *, id.id() AS username FROM User WHERE id.id() = '" + username + "' OR email = '"
-                            + username + "';");
             return response.take(0).getArray().get(0).get(User.class);
-        } catch (Exception e) {
+        } catch (NullPointerException e) {
             return null;
         }
     }
 
+    public boolean isUsernameAvailable(String username) {
path: byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/UserRepo.java
user: login: sourcery-ai[bot]
body: **🚨 suggestion (security):** Consider parameterizing query strings to avoid injection risks.

The query is constructed using string concatenation, which can be risky if the input is not fully sanitized. Using parameter binding would improve security and robustness.

Suggested implementation:

```java
    public boolean isUsernameAvailable(String username) {
        Response response = db.query("SELECT * FROM User WHERE username = ?;", username);
        return response.take(0).getArray().len() == 0;
    }

```

```java
    public boolean isEmailAvailable(String email) {
        Response response = db.query("SELECT * FROM User WHERE email = ?;", email);
        return response.take(0).getArray().len() == 0;
    }

```

Ensure that the db.query method supports parameterized queries with placeholders. If it does not, additional modifications to the database access layer might be required to avoid SQL injection risks.
created_at: 2025-04-20T20:46:46Z
author_association: NONE
diff_hunk: @@ -0,0 +1,86 @@
+package com.ufpr.byteassist_backend.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.validation.BindException;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@ControllerAdvice
+public class GlobalExceptionHandler {
+
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public ResponseEntity<ErrorResponse> handleValidationExceptions(MethodArgumentNotValidException ex) {
+        BindingResult bindingResult = ex.getBindingResult();
+        Map<String, String> errors = new HashMap<>();
+
+        for (FieldError error : bindingResult.getFieldErrors()) {
+            errors.put(error.getField(), error.getDefaultMessage());
+        }
+
+        ErrorResponse errorResponse = ErrorResponse.builder()
+                .message("Validation Error")
+                .statusCode(HttpStatus.BAD_REQUEST.value())
+                .errorCode(HttpStatus.BAD_REQUEST)
+                .errorDescription("The provided data is invalid")
+                .build();
+
+        // Manually set validation errors since builder may not be updated
+        errorResponse.setValidationErrors(errors);
+
+        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
+    }
+
+    @ExceptionHandler(BindException.class)
+    public ResponseEntity<ErrorResponse> handleBindExceptions(BindException ex) {
+        Map<String, String> errors = new HashMap<>();
+        ex.getBindingResult().getAllErrors().forEach((error) -> {
+            String fieldName = ((FieldError) error).getField();
+            String errorMessage = error.getDefaultMessage();
+            errors.put(fieldName, errorMessage);
+        });
+
+        ErrorResponse errorResponse = ErrorResponse.builder()
+                .message("Validation Error")
+                .statusCode(HttpStatus.BAD_REQUEST.value())
+                .errorCode(HttpStatus.BAD_REQUEST)
+                .errorDescription("The provided data contains validation errors")
+                .build();
+
+        // Manually set validation errors
+        errorResponse.setValidationErrors(errors);
+
+        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
+    }
+
+    @ExceptionHandler(HttpMessageNotReadableException.class)
+    public ResponseEntity<ErrorResponse> handleHttpMessageNotReadable(HttpMessageNotReadableException ex) {
+        ErrorResponse errorResponse = ErrorResponse.builder()
+                .message("Invalid request format")
+                .statusCode(HttpStatus.BAD_REQUEST.value())
+                .errorCode(HttpStatus.BAD_REQUEST)
+                .errorDescription("The request body could not be read. Ensure it's in the correct format.")
+                .build();
+
+        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
+    }
+
+    @ExceptionHandler(Exception.class)
+    public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex) {
+        ErrorResponse errorResponse = ErrorResponse.builder()
+                .message("Internal server error")
+                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR.value())
+                .errorCode(HttpStatus.INTERNAL_SERVER_ERROR)
+                .errorDescription("An unexpected error occurred: " + ex.getMessage())
path: byteassist-backend/src/main/java/com/ufpr/byteassist_backend/exception/GlobalExceptionHandler.java
user: login: sourcery-ai[bot]
body: **🚨 suggestion (security):** Avoid exposing internal exception details in error responses.

Including the exception message in the error description may leak internal information. Consider logging the detailed error internally and returning a generic error message to the client.

Suggested implementation:

```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

```

```java
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex) {
        logger.error("An unexpected error occurred", ex);
        ErrorResponse errorResponse = ErrorResponse.builder()
                .message("Internal server error")
                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR.value())
                .errorCode(HttpStatus.INTERNAL_SERVER_ERROR)
                .errorDescription("An unexpected error occurred. Please contact support if the issue persists.")
                .build();

```

Ensure that the logging library (e.g., slf4j) is included in your project dependencies and that consistency in logging is maintained across the project.
Also, review the file to confirm that no other endpoints expose similar exception details.
created_at: 2025-04-20T20:46:46Z
author_association: NONE
diff_hunk: @@ -0,0 +1,26 @@
+package com.ufpr.byteassist_backend.repository;
+
+import org.springframework.stereotype.Repository;
+
+import com.surrealdb.RecordId;
+import com.surrealdb.Surreal;
+import com.ufpr.byteassist_backend.model.Person;
+import com.ufpr.byteassist_backend.service.DatabaseService;
+
+@Repository
+public class PersonRepo {
+    private final Surreal db;
+
+    public PersonRepo(DatabaseService databaseService) {
+        this.db = databaseService.getDatabase();
+    }
+
+    public RecordId createPerson(Person person, String username) {
+        System.out.println("Creating person with username: " + username);
path: byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/PersonRepo.java
user: login: sourcery-ai[bot]
body: **suggestion:** Use a logging framework instead of System.out.println for production code.

Relying on System.out.println can be problematic in production environments since it lacks flexibility and proper log management. Integrating a logging framework would be beneficial.

Suggested implementation:

```java
@Repository
public class PersonRepo {

    private static final Logger logger = LoggerFactory.getLogger(PersonRepo.class);

```

```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

```

```java
        logger.info("Creating person with username: " + username);

```

```java
        logger.info("Person details: " + person);

```

```java
        logger.info("Person created successfully with ID: " + created.getId());

```
created_at: 2025-04-20T20:46:46Z
author_association: NONE
diff_hunk: @@ -24,4 +31,21 @@ public ResponseEntity<Object> login(@RequestParam String username, @RequestParam
         System.out.println("Login request received with username: " + username + " and password: " + password);
         return authService.login(username, password);
     }
+
+    @PostMapping(value = "/register", consumes = { MediaType.APPLICATION_FORM_URLENCODED_VALUE })
+    public ResponseEntity<Object> registerForm(@Valid @ModelAttribute RegistrationRequestDTO registrationRequest,
+            BindingResult bindingResult) {
+        System.out.println("Form registration request received: " + registrationRequest);
path: byteassist-backend/src/main/java/com/ufpr/byteassist_backend/controller/AuthController.java
user: login: sourcery-ai[bot]
body: **suggestion:** Replace direct console logging with a logging framework.

The direct use of System.out.println in the controller for logging incoming requests should be replaced with a proper logging solution to allow configurable logging levels and better production diagnostics.

Suggested implementation:

```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RestController
@RequestMapping("/auth")
public class AuthController {
    private static final Logger logger = LoggerFactory.getLogger(AuthController.class);

```

```java
        logger.info("Login request received with username: {} and password: {}", username, password);

```

```java
        logger.info("Form registration request received: {}", registrationRequest);

```
created_at: 2025-04-20T20:46:46Z
author_association: NONE
diff_hunk: @@ -49,4 +69,99 @@ public ResponseEntity<Object> login(String username, String password) {
                 .build();
         return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED);
     }
+
+    public ResponseEntity<Object> register(RegistrationRequestDTO user, BindingResult bindingResult) {
path: byteassist-backend/src/main/java/com/ufpr/byteassist_backend/service/AuthService.java
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting validation, business rules, and date conversion into helper methods to improve code readability and maintainability by isolating concerns within the register method, such as collecting errors and parsing the date of birth.

Consider extracting the validation, business rules, and date conversion into helper methods. For example:

1. **Aggregate Errors**  
   Create a method to consolidate Jakarta validation and business rule checks:

   ```java
   private Map<String, String> collectErrors(RegistrationRequestDTO user, BindingResult bindingResult) {
       Map<String, String> errors = new HashMap<>();
       for (FieldError error : bindingResult.getFieldErrors()) {
           errors.put(error.getField(), error.getDefaultMessage());
       }
       if (!userRepo.isUsernameAvailable(user.getUsername())) {
           errors.put("username availability", "Username is already taken");
       }
       if (!userRepo.isEmailAvailable(user.getEmail())) {
           errors.put("email availability", "Email is already in use");
       }
       return errors;
   }
   ```

2. **Separate Date Parsing**  
   Extract the date conversion into its own helper:

   ```java
   private ZonedDateTime parseDob(String dob, Map<String, String> errors) {
       try {
           LocalDate localDate = LocalDate.parse(dob);
           return localDate.atStartOfDay(ZoneId.systemDefault());
       } catch (DateTimeParseException e) {
           errors.put("dob", "Invalid date format. Please use YYYY-MM-DD");
           return null;
       }
   }
   ```

3. **Simplify the `register()` Method**  
   Then update your `register()` method to delegate these responsibilities:

   ```java
   public ResponseEntity<Object> register(RegistrationRequestDTO user, BindingResult bindingResult) {
       Map<String, String> errors = collectErrors(user, bindingResult);
       ZonedDateTime dobZoned = parseDob(user.getDob(), errors);

       if (!errors.isEmpty()) {
           ErrorResponse errorResponse = ErrorResponse.builder()
                   .message("Validation Error")
                   .errorCode(HttpStatus.BAD_REQUEST)
                   .statusCode(HttpStatus.BAD_REQUEST.value())
                   .errorDescription("The provided registration data is invalid")
                   .build();
           errorResponse.setValidationErrors(errors);
           return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
       }

       user.setPassword(passwordEncoder.encode(user.getPassword()));
       // ... proceed with assembling Person, User and persisting them
       // (remaining unchanged)

       return new ResponseEntity<>(HttpStatus.CREATED);
   }
   ```

This refactoring isolates concerns and reduces the cognitive load in the main registration method while keeping functionality intact.
created_at: 2025-04-20T20:46:46Z
author_association: NONE

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-04-20T20:45:48Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide by Sourcery

This pull request introduces a user registration endpoint with comprehensive validation and error handling. It enhances the authentication service to validate usernames and emails, and improves user retrieval with debug logging. The changes include new data models, repositories, and controller endpoints to support the user registration functionality.

#### Sequence diagram for user registration

```mermaid
sequenceDiagram
    participant U as User
    participant AC as AuthController
    participant AS as AuthService
    participant UR as UserRepo
    participant PR as PersonRepo
    participant DB as Database

    U->>AC: POST /auth/register with RegistrationRequestDTO
    AC->>AS: register(RegistrationRequestDTO, BindingResult)
    alt Validation Errors
        AS->>AS: Validate RegistrationRequestDTO
        AS-->>AC: ResponseEntity with ErrorResponse (validation errors)
        AC-->>U: HTTP 400 with validation errors
    else No Validation Errors
        AS->>UR: isUsernameAvailable(username)
        UR->>DB: Query User table for username
        DB-->>UR: Result
        alt Username Unavailable
            UR-->>AS: false
            AS-->>AC: ResponseEntity with ErrorResponse (username unavailable)
            AC-->>U: HTTP 409 Conflict
        else Username Available
            UR-->>AS: true
            AS->>UR: isEmailAvailable(email)
            UR->>DB: Query User table for email
            DB-->>UR: Result
            alt Email Unavailable
                UR-->>AS: false
                AS-->>AC: ResponseEntity with ErrorResponse (email unavailable)
                AC-->>U: HTTP 409 Conflict
            else Email Available
                UR-->>AS: true
                AS->>PR: createPerson(Person, username)
                PR->>DB: Create Person record
                DB-->>PR: RecordId
                PR-->>AS: RecordId
                AS->>UR: createUser(User)
                UR->>DB: Create User record
                DB-->>UR: Success
                UR-->>AS: Success
                AS-->>AC: ResponseEntity with HTTP 201 Created
                AC-->>U: HTTP 201 Created
            end
        end
    end
```

#### Updated class diagram for ErrorResponse

```mermaid
classDiagram
    class ErrorResponse {
        -String message
        -int statusCode
        -HttpStatus errorCode
        -String errorDescription
        -Map~String, String~ validationErrors
    }
    note for ErrorResponse "Includes validation errors"
```

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Implemented user registration endpoint with validation and error handling. | <ul><li>Added a new `/register` endpoint to `AuthController` to handle user registration requests.</li><li>Created `RegistrationRequestDTO` to encapsulate user registration data with Jakarta validation annotations.</li><li>Implemented validation logic in `AuthService` to check for username and email availability.</li><li>Added date of birth validation and conversion to `ZonedDateTime`.</li><li>Implemented error handling to return detailed validation errors to the client.</li><li>Created `Person`, `PersonAddress`, and `PersonName` models to store user details.</li><li>Implemented `PersonRepo` to handle the creation of person records.</li><li>Modified `AuthService` to create a new user and person record upon successful registration.</li></ul> | `byteassist-backend/src/main/java/com/ufpr/byteassist_backend/service/AuthService.java`<br/>`byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/UserRepo.java`<br/>`byteassist-backend/src/main/java/com/ufpr/byteassist_backend/controller/AuthController.java`<br/>`byteassist-backend/pom.xml`<br/>`byteassist-backend/src/main/java/com/ufpr/byteassist_backend/exception/ErrorResponse.java`<br/>`byteassist-backend/src/main/java/com/ufpr/byteassist_backend/exception/GlobalExceptionHandler.java`<br/>`byteassist-backend/src/main/java/com/ufpr/byteassist_backend/dto/RegistrationRequestDTO.java`<br/>`byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/PersonRepo.java`<br/>`byteassist-backend/src/main/java/com/ufpr/byteassist_backend/model/Person.java`<br/>`byteassist-backend/src/main/java/com/ufpr/byteassist_backend/model/PersonAddress.java`<br/>`byteassist-backend/src/main/java/com/ufpr/byteassist_backend/model/PersonName.java` |
| Enhanced authentication service with username and email validation. | <ul><li>Added `isUsernameAvailable` and `isEmailAvailable` methods to `UserRepo` to check for uniqueness.</li><li>Implemented `/validate/username/{username}` and `/validate/email/{email}` endpoints in `AuthController`.</li><li>Added validation for username to only allow lowercase letters, numbers, dots, underscores, and hyphens.</li><li>Implemented new exception handler to handle validation errors.</li></ul> | `byteassist-backend/src/main/java/com/ufpr/byteassist_backend/service/AuthService.java`<br/>`byteassist-backend/src/main/java/com/ufpr/byteassist_backend/repository/UserRepo.java`<br/>`byteassist-backend/src/main/java/com/ufpr/byteassist_backend/controller/AuthController.java` |
| Improved user retrieval with debug logging. | <ul><li>Added debug logging to the `login` method in `AuthService` to print password hashes and user details.</li></ul> | `byteassist-backend/src/main/java/com/ufpr/byteassist_backend/service/AuthService.java` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!
- **Generate a plan of action for an issue:** Comment `@sourcery-ai plan` on
  an issue to generate a plan of action for it.

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: NONE
user: login: naming-conventions-bot[bot]
created_at: 2025-04-20T20:45:50Z
body: Please follow naming conventions! 😿
author_association: NONE