diff --git a/backend/build.gradle b/backend/build.gradle index dd7ff6c..101e918 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -30,10 +30,12 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-webmvc' + implementation 'org.mapstruct:mapstruct:1.6.3' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3' testImplementation 'org.springframework.boot:spring-boot-starter-data-jdbc-test' testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test' testImplementation 'org.springframework.boot:spring-boot-starter-validation-test' diff --git a/backend/postman_collection.json b/backend/postman_collection.json new file mode 100644 index 0000000..a67c35b --- /dev/null +++ b/backend/postman_collection.json @@ -0,0 +1,232 @@ +{ + "info": { + "name": "Users API - Server Manager", + "description": "Collection de testes para a API de Usuários", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Users", + "item": [ + { + "name": "Create User", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstName\": \"João\",\n \"lastName\": \"Silva\",\n \"email\": \"joao.silva@hittelco.com\",\n \"password\": \"senha123\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/users", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Create User - Test 2", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstName\": \"Maria\",\n \"lastName\": \"Santos\",\n \"email\": \"maria.santos@accesscommunications.com\",\n \"password\": \"senha456\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/users", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Create User - Invalid Domain", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstName\": \"Carlos\",\n \"lastName\": \"Oliveira\",\n \"email\": \"carlos@gmail.com\",\n \"password\": \"senha789\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/users", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Get All Users", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/users", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Get User by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/users/{{user_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "users", + "{{user_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Get User by Username", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/users/username/joao.silva@hittelco.com", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "users", + "username", + "joao.silva@hittelco.com" + ] + } + }, + "response": [] + }, + { + "name": "Get User by Email", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/users/email/maria.santos@accesscommunications.com", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "users", + "email", + "maria.santos@accesscommunications.com" + ] + } + }, + "response": [] + }, + { + "name": "Update User", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstName\": \"João Paulo\",\n \"lastName\": \"Silva Santos\",\n \"email\": \"joao.paulo@hittelco.com\",\n \"password\": \"novaSenha123\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/users/{{user_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "users", + "{{user_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete User", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{base_url}}/api/users/{{user_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "users", + "{{user_id}}" + ] + } + }, + "response": [] + } + ] + } + ], + "variable": [ + { + "key": "base_url", + "value": "http://localhost:8080", + "type": "string" + }, + { + "key": "user_id", + "value": "", + "type": "string" + } + ] +} + diff --git a/backend/src/main/java/com/hitcommunications/servermanager/BackendApplication.java b/backend/src/main/java/com/hitcommunications/servermanager/BackendApplication.java index aacf78c..499432d 100644 --- a/backend/src/main/java/com/hitcommunications/servermanager/BackendApplication.java +++ b/backend/src/main/java/com/hitcommunications/servermanager/BackendApplication.java @@ -17,4 +17,4 @@ public class BackendApplication { public String home() { return "Hello, World!"; } -} +} \ No newline at end of file diff --git a/backend/src/main/java/com/hitcommunications/servermanager/controllers/UsersController.java b/backend/src/main/java/com/hitcommunications/servermanager/controllers/UsersController.java new file mode 100644 index 0000000..6f22abb --- /dev/null +++ b/backend/src/main/java/com/hitcommunications/servermanager/controllers/UsersController.java @@ -0,0 +1,56 @@ +package com.hitcommunications.servermanager.controllers; + +import com.hitcommunications.servermanager.model.dtos.NewUserDTO; +import com.hitcommunications.servermanager.model.dtos.UserDTO; +import com.hitcommunications.servermanager.services.UsersService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/users") +@RequiredArgsConstructor +public class UsersController { + + private final UsersService usersService; + + @PostMapping + public ResponseEntity create(@RequestBody @Valid NewUserDTO crateDTO) throws IllegalAccessException { + return ResponseEntity.ok().body(usersService.create(crateDTO)); + } + + @GetMapping("/{id}") + public ResponseEntity getById(@PathVariable UUID id) { + return ResponseEntity.ok().body(usersService.getById(id)); + } + + @GetMapping("/username/{username}") + public ResponseEntity getByUsername(@PathVariable String username) { + return ResponseEntity.ok().body(usersService.getByUsername(username)); + } + + @GetMapping("/email/{email}") + public ResponseEntity getByEmail(@PathVariable String email) { + return ResponseEntity.ok().body(usersService.getByEmail(email)); + } + + @GetMapping + public ResponseEntity> getAll() { + return ResponseEntity.ok().body(usersService.getAll()); + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable UUID id, @RequestBody @Valid NewUserDTO updateDTO) throws IllegalAccessException { + return ResponseEntity.ok().body(usersService.update(id, updateDTO)); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable UUID id) { + usersService.delete(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/hitcommunications/servermanager/mappers/UsersMapper.java b/backend/src/main/java/com/hitcommunications/servermanager/mappers/UsersMapper.java new file mode 100644 index 0000000..ce2b505 --- /dev/null +++ b/backend/src/main/java/com/hitcommunications/servermanager/mappers/UsersMapper.java @@ -0,0 +1,21 @@ +package com.hitcommunications.servermanager.mappers; + +import com.hitcommunications.servermanager.model.Users; +import com.hitcommunications.servermanager.model.dtos.NewUserDTO; +import com.hitcommunications.servermanager.model.dtos.UserDTO; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; + +import java.util.List; + +@Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) +public interface UsersMapper { + Users toEntity(NewUserDTO createDTO); + UserDTO toDTO(Users entity); + List toDTO(List entities); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + Users partialUpdate(NewUserDTO updateDTO, @MappingTarget Users entity); +} diff --git a/backend/src/main/java/com/hitcommunications/servermanager/model/Users.java b/backend/src/main/java/com/hitcommunications/servermanager/model/Users.java index 7e58c17..a0c78e8 100644 --- a/backend/src/main/java/com/hitcommunications/servermanager/model/Users.java +++ b/backend/src/main/java/com/hitcommunications/servermanager/model/Users.java @@ -58,6 +58,6 @@ public class Users { String firstPart = this.firstName != null && this.firstName.length() >= 3 ? this.firstName.substring(0, 3) : (this.firstName != null ? this.firstName : ""); String secondPart = this.lastName != null && this.lastName.length() >= 3 ? this.lastName.substring(0, 3) : (this.lastName != null ? this.lastName : ""); this.username = firstPart + secondPart; - return this.username; + return this.username.toLowerCase(); } } diff --git a/backend/src/main/java/com/hitcommunications/servermanager/model/dtos/NewUserDTO.java b/backend/src/main/java/com/hitcommunications/servermanager/model/dtos/NewUserDTO.java new file mode 100644 index 0000000..99de0b4 --- /dev/null +++ b/backend/src/main/java/com/hitcommunications/servermanager/model/dtos/NewUserDTO.java @@ -0,0 +1,12 @@ +package com.hitcommunications.servermanager.model.dtos; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record NewUserDTO( + @NotBlank String firstName, + @NotBlank String lastName, + @NotBlank @Email String email, + @NotBlank String password +) { +} diff --git a/backend/src/main/java/com/hitcommunications/servermanager/model/dtos/UserDTO.java b/backend/src/main/java/com/hitcommunications/servermanager/model/dtos/UserDTO.java new file mode 100644 index 0000000..348984f --- /dev/null +++ b/backend/src/main/java/com/hitcommunications/servermanager/model/dtos/UserDTO.java @@ -0,0 +1,15 @@ +package com.hitcommunications.servermanager.model.dtos; + +import java.sql.Timestamp; + +public record UserDTO( + String id, + String username, + String firstName, + String lastName, + String email, + Timestamp createdAt, + Timestamp updatedAt, + Timestamp lastLogin +) { +} diff --git a/backend/src/main/java/com/hitcommunications/servermanager/repositories/UsersRepository.java b/backend/src/main/java/com/hitcommunications/servermanager/repositories/UsersRepository.java new file mode 100644 index 0000000..7f59dd2 --- /dev/null +++ b/backend/src/main/java/com/hitcommunications/servermanager/repositories/UsersRepository.java @@ -0,0 +1,12 @@ +package com.hitcommunications.servermanager.repositories; + +import com.hitcommunications.servermanager.model.Users; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface UsersRepository extends JpaRepository { + Optional findByUsername(String username); + Optional findByEmail(String email); +} diff --git a/backend/src/main/java/com/hitcommunications/servermanager/services/UsersService.java b/backend/src/main/java/com/hitcommunications/servermanager/services/UsersService.java new file mode 100644 index 0000000..8c391d8 --- /dev/null +++ b/backend/src/main/java/com/hitcommunications/servermanager/services/UsersService.java @@ -0,0 +1,94 @@ +package com.hitcommunications.servermanager.services; + +import com.hitcommunications.servermanager.mappers.UsersMapper; +import com.hitcommunications.servermanager.model.Users; +import com.hitcommunications.servermanager.model.dtos.NewUserDTO; +import com.hitcommunications.servermanager.model.dtos.UserDTO; +import com.hitcommunications.servermanager.repositories.UsersRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class UsersService { + private final UsersMapper mapper; + private final UsersRepository repo; + + private final String ALLOWED_DOMAIN = Arrays.asList("hittelco.com", "accesscommunications.com").toString(); + + public UserDTO create(NewUserDTO createDTO) throws IllegalAccessException { + String domain = getDomain(createDTO.email()); + if (!ALLOWED_DOMAIN.contains(domain)) { + throw new IllegalAccessException("Email domain not allowed: " + domain); + } + + repo.findByEmail(createDTO.email()).ifPresent(entity -> { + throw new RuntimeException("Email already exists: " + createDTO.email()); + }); + + Users entity = mapper.toEntity(createDTO); + entity = repo.save(entity); + return mapper.toDTO(entity); + } + + public UserDTO getById(UUID id) { + return repo.findById(id) + .map(mapper::toDTO) + .orElseThrow(() -> new RuntimeException("User not found with id: " + id)); + } + + public UserDTO getByUsername(String username) { + return repo.findByUsername(username) + .map(mapper::toDTO) + .orElseThrow(() -> new RuntimeException("User not found with username: " + username)); + } + + public UserDTO getByEmail(String email) { + return repo.findByEmail(email) + .map(mapper::toDTO) + .orElseThrow(() -> new RuntimeException("User not found with email: " + email)); + } + + public List getAll() { + return repo.findAll() + .stream() + .map(mapper::toDTO) + .toList(); + } + + public UserDTO update(UUID id, NewUserDTO updateDTO) throws IllegalAccessException { + Users entity = repo.findById(id) + .orElseThrow(() -> new RuntimeException("User not found with id: " + id)); + + String domain = getDomain(updateDTO.email()); + if (!ALLOWED_DOMAIN.contains(domain)) { + throw new IllegalAccessException("Email domain not allowed: " + domain); + } + + // Check if email already exists (excluding current user) + repo.findByEmail(updateDTO.email()).ifPresent(existingUser -> { + if (!existingUser.getId().equals(id)) { + throw new RuntimeException("Email already exists: " + updateDTO.email()); + } + }); + + mapper.partialUpdate(updateDTO, entity); + entity = repo.save(entity); + return mapper.toDTO(entity); + } + + public void delete(UUID id) { + if (!repo.existsById(id)) { + throw new RuntimeException("User not found with id: " + id); + } + repo.deleteById(id); + } + + private String getDomain(String email) { + return email.substring(email.indexOf("@") + 1); + } +}