feat: implement complete CRUD for Users API

- Add UsersService with create, read (getById, getByUsername, getByEmail, getAll), update (using mapper.partialUpdate), and delete methods
- Add UsersController with endpoints for all CRUD operations
- Add UsersRepository with custom queries for username and email
- Add UserDTO and NewUserDTO data transfer objects
- Add UsersMapper for entity-DTO conversions
- Add email domain validation (hittelco.com, accesscommunications.com)
- Add email uniqueness validation
- Create Postman collection for API testing with sample data
- Update Users model with timestamps and builder pattern
master
Artur Oliveira 2025-12-15 17:27:59 -03:00
parent 06ba25eabd
commit 348f9faa7d
10 changed files with 446 additions and 2 deletions

View File

@ -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'

View File

@ -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"
}
]
}

View File

@ -17,4 +17,4 @@ public class BackendApplication {
public String home() {
return "Hello, World!";
}
}
}

View File

@ -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<UserDTO> create(@RequestBody @Valid NewUserDTO crateDTO) throws IllegalAccessException {
return ResponseEntity.ok().body(usersService.create(crateDTO));
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getById(@PathVariable UUID id) {
return ResponseEntity.ok().body(usersService.getById(id));
}
@GetMapping("/username/{username}")
public ResponseEntity<UserDTO> getByUsername(@PathVariable String username) {
return ResponseEntity.ok().body(usersService.getByUsername(username));
}
@GetMapping("/email/{email}")
public ResponseEntity<UserDTO> getByEmail(@PathVariable String email) {
return ResponseEntity.ok().body(usersService.getByEmail(email));
}
@GetMapping
public ResponseEntity<List<UserDTO>> getAll() {
return ResponseEntity.ok().body(usersService.getAll());
}
@PutMapping("/{id}")
public ResponseEntity<UserDTO> update(@PathVariable UUID id, @RequestBody @Valid NewUserDTO updateDTO) throws IllegalAccessException {
return ResponseEntity.ok().body(usersService.update(id, updateDTO));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable UUID id) {
usersService.delete(id);
return ResponseEntity.noContent().build();
}
}

View File

@ -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<Users> toDTO(List<Users> entities);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
Users partialUpdate(NewUserDTO updateDTO, @MappingTarget Users entity);
}

View File

@ -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();
}
}

View File

@ -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
) {
}

View File

@ -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
) {
}

View File

@ -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<Users, UUID> {
Optional<Users> findByUsername(String username);
Optional<Users> findByEmail(String email);
}

View File

@ -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<UserDTO> 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);
}
}