diff --git a/ReadmeFeatures/README.md b/ReadmeFeatures/README.md new file mode 100644 index 0000000..e19bde0 --- /dev/null +++ b/ReadmeFeatures/README.md @@ -0,0 +1,102 @@ +# Documentação das Features + +Esta pasta contém a documentação detalhada das funcionalidades implementadas no projeto API Viagem. + +## 📁 Estrutura da Documentação + +### 🔐 Sistema de Roles +- **[ROLE_API_DOCUMENTATION.md](./ROLE_API_DOCUMENTATION.md)** - Documentação completa da API de gerenciamento de roles +- **[USER_ROLE_RELATIONSHIP.md](./USER_ROLE_RELATIONSHIP.md)** - Documentação do relacionamento entre usuários e roles + +## 🚀 Features Implementadas + +### 1. Sistema de Roles +- ✅ Modelo Role com enum RoleType (USER, ADMIN) +- ✅ CRUD completo para roles +- ✅ Endpoints REST para gerenciamento +- ✅ Validações e tratamento de exceções + +### 2. Relacionamento User-Role +- ✅ Relacionamento Many-to-Many entre User e Role +- ✅ Atribuição automática de role padrão (USER) para novos usuários +- ✅ Endpoints para gerenciar roles de usuários +- ✅ Métodos utilitários para verificação de permissões + +## 📋 Como Usar + +### Criando Roles +```bash +# Criar role USER +curl -X POST http://localhost:8080/api/roles \ + -H "Content-Type: application/json" \ + -d '{"name": "USER", "description": "Usuário comum"}' + +# Criar role ADMIN +curl -X POST http://localhost:8080/api/roles \ + -H "Content-Type: application/json" \ + -d '{"name": "ADMIN", "description": "Administrador"}' +``` + +### Gerenciando Roles de Usuários +```bash +# Promover usuário a administrador +curl -X POST http://localhost:8080/api/users/{userId}/roles/ADMIN + +# Verificar se usuário é admin +curl -X GET http://localhost:8080/api/users/{userId}/has-role/ADMIN + +# Listar usuários administradores +curl -X GET http://localhost:8080/api/users/admins +``` + +## 🔧 Arquitetura + +### Camadas Implementadas +- **Model**: Role, RoleType, relacionamento com User +- **Repository**: RoleRepository, métodos adicionais no UserRepository +- **Service**: RoleService, UserRoleService +- **Controller**: RoleController, UserRoleController +- **DTO**: RoleRequestDTO, RoleResponseDTO + +### Padrões Utilizados +- **Repository Pattern**: Para acesso a dados +- **Service Layer**: Para lógica de negócio +- **DTO Pattern**: Para transferência de dados +- **REST API**: Endpoints padronizados + +## 📊 Banco de Dados + +### Tabelas Criadas +- `roles` - Armazena as roles do sistema +- `user_roles` - Tabela de relacionamento Many-to-Many + +### Relacionamentos +- User ↔ Role (Many-to-Many) +- Tabela intermediária: user_roles + +## 🔒 Segurança + +### Implementações de Segurança +- Validação de dados com Bean Validation +- Tratamento de exceções personalizadas +- Verificação de permissões antes de operações +- Atribuição automática de role padrão + +### Próximos Passos de Segurança +- [ ] Implementar middleware de autenticação +- [ ] Adicionar logs de auditoria +- [ ] Implementar cache para consultas de roles +- [ ] Criar sistema de hierarquia de roles + +## 📈 Próximas Features + +- [ ] Sistema de permissões granular +- [ ] Middleware de autorização +- [ ] Logs de auditoria para mudanças de roles +- [ ] Interface web para gerenciamento de roles +- [ ] Sistema de convites para roles administrativas + +--- + +**Última atualização**: $(date) +**Versão**: 1.0.0 diff --git a/ReadmeFeatures/ROLE_API_DOCUMENTATION.md b/ReadmeFeatures/ROLE_API_DOCUMENTATION.md new file mode 100644 index 0000000..3af986d --- /dev/null +++ b/ReadmeFeatures/ROLE_API_DOCUMENTATION.md @@ -0,0 +1,165 @@ +# Documentação da API de Roles + +## Visão Geral +Esta API permite gerenciar roles no sistema, incluindo as roles de **USER** e **ADMIN**. + +## Endpoints Disponíveis + +### 1. Buscar Todas as Roles +```http +GET /api/roles +``` + +**Resposta de Sucesso (200):** +```json +[ + { + "id": "123e4567-e89b-12d3-a456-426614174000", + "name": "USER", + "description": "Usuário comum do sistema", + "createdAt": "2024-01-01T10:00:00Z", + "updatedAt": "2024-01-01T10:00:00Z" + }, + { + "id": "123e4567-e89b-12d3-a456-426614174001", + "name": "ADMIN", + "description": "Administrador com acesso total ao sistema", + "createdAt": "2024-01-01T10:00:00Z", + "updatedAt": "2024-01-01T10:00:00Z" + } +] +``` + +### 2. Buscar Role por ID +```http +GET /api/roles/{id} +``` + +**Parâmetros:** +- `id` (UUID): ID único da role + +**Resposta de Sucesso (200):** +```json +{ + "id": "123e4567-e89b-12d3-a456-426614174000", + "name": "USER", + "description": "Usuário comum do sistema", + "createdAt": "2024-01-01T10:00:00Z", + "updatedAt": "2024-01-01T10:00:00Z" +} +``` + +**Resposta de Erro (404):** +```json +Role não encontrada +``` + +### 3. Buscar Role por Nome +```http +GET /api/roles/name/{name} +``` + +**Parâmetros:** +- `name` (RoleType): USER ou ADMIN + +**Exemplo:** +```http +GET /api/roles/name/USER +``` + +### 4. Cadastrar Nova Role +```http +POST /api/roles +``` + +**Body (JSON):** +```json +{ + "name": "USER", + "description": "Usuário comum do sistema" +} +``` + +**Resposta de Sucesso (201):** +```json +{ + "id": "123e4567-e89b-12d3-a456-426614174000", + "name": "USER", + "description": "Usuário comum do sistema", + "createdAt": "2024-01-01T10:00:00Z", + "updatedAt": "2024-01-01T10:00:00Z" +} +``` + +**Resposta de Erro (400):** +```json +Role já existe com o tipo: USER +``` + +### 5. Atualizar Role +```http +PUT /api/roles/{id} +``` + +**Body (JSON):** +```json +{ + "name": "USER", + "description": "Descrição atualizada" +} +``` + +### 6. Deletar Role por ID +```http +DELETE /api/roles/{id} +``` + +**Resposta de Sucesso (204):** +``` +Sem conteúdo +``` + +### 7. Deletar Role por Nome +```http +DELETE /api/roles/name/{name} +``` + +**Exemplo:** +```http +DELETE /api/roles/name/USER +``` + +## Tipos de Role Disponíveis + +- **USER**: Usuário comum do sistema +- **ADMIN**: Administrador com acesso total ao sistema + +## Códigos de Status HTTP + +- **200**: Sucesso +- **201**: Criado com sucesso +- **204**: Deletado com sucesso (sem conteúdo) +- **400**: Requisição inválida (role já existe, dados inválidos) +- **404**: Role não encontrada + +## Exemplo de Uso com cURL + +### Cadastrar Role USER: +```bash +curl -X POST http://localhost:8080/api/roles \ + -H "Content-Type: application/json" \ + -d '{ + "name": "USER", + "description": "Usuário comum do sistema" + }' +``` + +### Buscar Todas as Roles: +```bash +curl -X GET http://localhost:8080/api/roles +``` + +### Deletar Role por Nome: +```bash +curl -X DELETE http://localhost:8080/api/roles/name/USER +``` diff --git a/ReadmeFeatures/USER_ROLE_RELATIONSHIP.md b/ReadmeFeatures/USER_ROLE_RELATIONSHIP.md new file mode 100644 index 0000000..be39dcd --- /dev/null +++ b/ReadmeFeatures/USER_ROLE_RELATIONSHIP.md @@ -0,0 +1,197 @@ +# Relacionamento User-Role + +## Visão Geral +Este documento descreve o relacionamento Many-to-Many entre as entidades User e Role, permitindo que usuários possuam múltiplas roles e vice-versa. + +## Estrutura do Relacionamento + +### Tabela de Relacionamento +```sql +CREATE TABLE user_roles ( + user_id UUID REFERENCES users(id), + role_id UUID REFERENCES roles(id), + PRIMARY KEY (user_id, role_id) +); +``` + +### Entidades Modificadas + +#### User.java +- Adicionado campo `Set roles` +- Relacionamento Many-to-Many com Role +- Métodos utilitários para gerenciar roles + +#### Role.java +- Entidade independente com enum RoleType +- Relacionamento inverso com User + +## Funcionalidades Implementadas + +### UserRoleService +Serviço responsável por gerenciar o relacionamento entre usuários e roles: + +- `assignRoleToUser()` - Atribui role a usuário +- `removeRoleFromUser()` - Remove role de usuário +- `getUsersByRole()` - Busca usuários por tipo de role +- `getAdminUsers()` - Lista usuários administradores +- `getRegularUsers()` - Lista usuários regulares +- `userHasRole()` - Verifica se usuário possui role +- `setUserRoles()` - Define todas as roles de um usuário +- `assignDefaultRole()` - Atribui role padrão USER + +### UserRepository +Novos métodos adicionados: + +- `findByRoleType()` - Busca usuários por tipo de role +- `findAdminUsers()` - Busca usuários administradores +- `findRegularUsers()` - Busca usuários regulares +- `userHasRole()` - Verifica se usuário possui role + +## Endpoints da API User-Role + +### 1. Atribuir Role a Usuário +```http +POST /api/users/{userId}/roles/{roleType} +``` + +**Exemplo:** +```bash +curl -X POST http://localhost:8080/api/users/123e4567-e89b-12d3-a456-426614174000/roles/ADMIN +``` + +### 2. Remover Role de Usuário +```http +DELETE /api/users/{userId}/roles/{roleType} +``` + +### 3. Buscar Usuários por Role +```http +GET /api/users/by-role/{roleType} +``` + +**Exemplo:** +```bash +curl -X GET http://localhost:8080/api/users/by-role/USER +``` + +### 4. Listar Usuários Administradores +```http +GET /api/users/admins +``` + +### 5. Listar Usuários Regulares +```http +GET /api/users/regular-users +``` + +### 6. Verificar se Usuário Possui Role +```http +GET /api/users/{userId}/has-role/{roleType} +``` + +**Resposta:** +```json +true +``` + +### 7. Definir Roles do Usuário (Substitui todas) +```http +PUT /api/users/{userId}/roles +``` + +**Body:** +```json +["USER", "ADMIN"] +``` + +### 8. Buscar Roles de um Usuário +```http +GET /api/users/{userId}/roles +``` + +**Resposta:** +```json +[ + { + "id": "123e4567-e89b-12d3-a456-426614174000", + "name": "USER", + "description": "Usuário comum do sistema", + "createdAt": "2024-01-01T10:00:00Z", + "updatedAt": "2024-01-01T10:00:00Z" + } +] +``` + +## Integração com AuthService + +### Atribuição Automática de Role Padrão +Quando um usuário faz login via Google pela primeira vez, automaticamente recebe a role `USER`: + +```java +// No AuthService.getUserInfo() +if (user.getRoles() == null || user.getRoles().isEmpty()) { + userRoleService.assignDefaultRole(user); +} +``` + +## Métodos Utilitários no User + +### Verificar Role +```java +boolean isAdmin = user.hasRole(RoleType.ADMIN); +boolean isUser = user.hasRole(RoleType.USER); +``` + +### Adicionar/Remover Role +```java +user.addRole(role); +user.removeRole(role); +``` + +## Exemplos de Uso + +### Promover Usuário a Administrador +```bash +# 1. Criar role ADMIN se não existir +curl -X POST http://localhost:8080/api/roles \ + -H "Content-Type: application/json" \ + -d '{"name": "ADMIN", "description": "Administrador"}' + +# 2. Atribuir role ADMIN ao usuário +curl -X POST http://localhost:8080/api/users/{userId}/roles/ADMIN +``` + +### Verificar Permissões +```java +@Service +public class PermissionService { + + public boolean canAccessAdminPanel(UUID userId) { + return userRoleService.userHasRole(userId, RoleType.ADMIN); + } + + public boolean canAccessUserPanel(UUID userId) { + return userRoleService.userHasRole(userId, RoleType.USER); + } +} +``` + +## Códigos de Status HTTP + +- **200**: Sucesso +- **404**: Usuário ou Role não encontrado +- **400**: Dados inválidos + +## Considerações de Segurança + +1. **Validação de Permissões**: Sempre verificar roles antes de permitir acesso a funcionalidades administrativas +2. **Role Padrão**: Novos usuários recebem automaticamente a role USER +3. **Auditoria**: Considerar implementar logs de alterações de roles +4. **Hierarquia**: Roles podem ser expandidas para incluir hierarquias (ex: SUPER_ADMIN) + +## Próximos Passos + +1. Implementar validação de permissões em controllers +2. Adicionar logs de auditoria para mudanças de roles +3. Criar middleware de autenticação/autorização +4. Implementar cache para consultas frequentes de roles diff --git a/pom.xml b/pom.xml index 1f09bf5..8d5aa59 100644 --- a/pom.xml +++ b/pom.xml @@ -81,7 +81,48 @@ spring-boot-starter-webflux + + io.jsonwebtoken + jjwt-impl + 0.12.3 + runtime + + + + + io.jsonwebtoken + jjwt-api + 0.12.3 + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-test + test + + + io.jsonwebtoken + jjwt-api + 0.12.5 + + + + io.jsonwebtoken + jjwt-impl + 0.12.5 + runtime + + + + io.jsonwebtoken + jjwt-jackson + 0.12.5 + runtime + diff --git a/src/main/java/com/api/apiviagem/AdminInitializer.java b/src/main/java/com/api/apiviagem/AdminInitializer.java new file mode 100644 index 0000000..bf7ea84 --- /dev/null +++ b/src/main/java/com/api/apiviagem/AdminInitializer.java @@ -0,0 +1,68 @@ +package com.api.apiviagem; + +import com.api.apiviagem.model.AuthProvider; +import com.api.apiviagem.model.RoleType; +import com.api.apiviagem.model.User; +import com.api.apiviagem.model.Role; // Supondo que você tenha uma entidade Role +import com.api.apiviagem.repository.UserRepository; +import com.api.apiviagem.repository.RoleRepository; // Supondo que você tenha um repositório para Roles +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Set; + +@Component +@Profile("dev") // 👈 Importante! Só executa quando o perfil 'dev' está ativo. +public class AdminInitializer implements CommandLineRunner { + + private static final Logger logger = LoggerFactory.getLogger(AdminInitializer.class); + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoleRepository roleRepository; // Necessário para buscar a role de ADM + + // Injeta os dados do admin a partir do application-dev.properties + @Value("${admin.email}") + private String adminEmail; + + @Value("${admin.name}") + private String adminName; + + @Override + @Transactional + public void run(String... args) throws Exception { + logger.info("Verificando se o usuário administrador padrão existe..."); + + // A lógica que você sugeriu: verificar se o admin já existe + if (userRepository.findByEmail(adminEmail).isPresent()) { + logger.info("Usuário administrador já existe. Nenhuma ação necessária."); + return; + } + + logger.info("Usuário administrador não encontrado. Criando novo usuário admin..."); + RoleType roleType = RoleType.ADMIN; + + // Busca a role de admin ou cria se não existir (opcional, mas robusto) + Role adminRole = roleRepository.findByName(roleType) + .orElseGet(() -> roleRepository.save(new Role(roleType, roleType.getDescription()))); + + User adminUser = new User(); + adminUser.setEmail(adminEmail); + // NUNCA armazene a senha em texto plano. Sempre use o encoder. + adminUser.setName(adminName); + adminUser.setRoles(Set.of(adminRole)); + adminUser.setAuthProvider(AuthProvider.LOCAL); + + userRepository.save(adminUser); + + logger.info("Usuário administrador criado com sucesso. Email: {}", adminEmail); + } +} \ No newline at end of file diff --git a/src/main/java/com/api/apiviagem/DTO/request/GoogleTokenRequest.java b/src/main/java/com/api/apiviagem/DTO/request/GoogleTokenRequest.java new file mode 100644 index 0000000..693ff0e --- /dev/null +++ b/src/main/java/com/api/apiviagem/DTO/request/GoogleTokenRequest.java @@ -0,0 +1,4 @@ +package com.api.apiviagem.DTO.request; + +public record GoogleTokenRequest(String accessToken) { +} diff --git a/src/main/java/com/api/apiviagem/DTO/request/RoleRequestDTO.java b/src/main/java/com/api/apiviagem/DTO/request/RoleRequestDTO.java new file mode 100644 index 0000000..724059a --- /dev/null +++ b/src/main/java/com/api/apiviagem/DTO/request/RoleRequestDTO.java @@ -0,0 +1,12 @@ +package com.api.apiviagem.DTO.request; + +import com.api.apiviagem.model.RoleType; +import jakarta.validation.constraints.NotNull; + +public record RoleRequestDTO( + @NotNull(message = "Tipo da role é obrigatório") + RoleType name, + + String description +) { +} diff --git a/src/main/java/com/api/apiviagem/DTO/response/ApiErrorResponse.java b/src/main/java/com/api/apiviagem/DTO/response/ApiErrorResponse.java new file mode 100644 index 0000000..13b4e4c --- /dev/null +++ b/src/main/java/com/api/apiviagem/DTO/response/ApiErrorResponse.java @@ -0,0 +1,5 @@ +package com.api.apiviagem.DTO.response; + +import java.time.Instant; + +public record ApiErrorResponse(int status, String error, String message, Instant timestamp) {} \ No newline at end of file diff --git a/src/main/java/com/api/apiviagem/DTO/response/ErrorResponse.java b/src/main/java/com/api/apiviagem/DTO/response/ErrorResponse.java new file mode 100644 index 0000000..819e7ea --- /dev/null +++ b/src/main/java/com/api/apiviagem/DTO/response/ErrorResponse.java @@ -0,0 +1,4 @@ +package com.api.apiviagem.DTO.response; + +public record ErrorResponse(String error) { +} diff --git a/src/main/java/com/api/apiviagem/DTO/response/RoleResponseDTO.java b/src/main/java/com/api/apiviagem/DTO/response/RoleResponseDTO.java new file mode 100644 index 0000000..df7621d --- /dev/null +++ b/src/main/java/com/api/apiviagem/DTO/response/RoleResponseDTO.java @@ -0,0 +1,25 @@ +package com.api.apiviagem.DTO.response; + +import com.api.apiviagem.model.Role; +import com.api.apiviagem.model.RoleType; + +import java.time.Instant; +import java.util.UUID; + +public record RoleResponseDTO( + UUID id, + RoleType name, + String description, + Instant createdAt, + Instant updatedAt +) { + public static RoleResponseDTO fromRole(Role role) { + return new RoleResponseDTO( + role.getId(), + role.getName(), + role.getDescription(), + role.getCreatedAt(), + role.getUpdatedAt() + ); + } +} diff --git a/src/main/java/com/api/apiviagem/DTO/response/UserResponseDTO.java b/src/main/java/com/api/apiviagem/DTO/response/UserResponseDTO.java new file mode 100644 index 0000000..bc833bc --- /dev/null +++ b/src/main/java/com/api/apiviagem/DTO/response/UserResponseDTO.java @@ -0,0 +1,10 @@ +package com.api.apiviagem.DTO.response; + +import com.api.apiviagem.model.Role; + +import java.time.Instant; +import java.util.Set; +import java.util.UUID; + +public record UserResponseDTO(UUID id, String name, String email, String imageUrl, Set role, Instant createdAt, Instant updatedAt) { +} diff --git a/src/main/java/com/api/apiviagem/controller/AuthController.java b/src/main/java/com/api/apiviagem/controller/AuthController.java index dd2af73..f9b1b87 100644 --- a/src/main/java/com/api/apiviagem/controller/AuthController.java +++ b/src/main/java/com/api/apiviagem/controller/AuthController.java @@ -1,5 +1,7 @@ package com.api.apiviagem.controller; +import com.api.apiviagem.DTO.request.GoogleTokenRequest; +import com.api.apiviagem.DTO.response.ErrorResponse; import com.api.apiviagem.model.User; import com.api.apiviagem.service.AuthService; import org.springframework.beans.factory.annotation.Autowired; @@ -7,7 +9,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.List; @RestController @RequestMapping("/api/v1/auth") @@ -15,19 +16,18 @@ public class AuthController { @Autowired private AuthService authService; - @GetMapping - public ResponseEntity> GetAllUsers() { - return ResponseEntity.status(HttpStatus.OK).body(authService.getUsers()); - } + @PostMapping("/google/callback") + public ResponseEntity handleGoogleCallback(@RequestBody GoogleTokenRequest request) { + String accessToken = request.accessToken(); - @GetMapping("/google/callback") - public ResponseEntity handleGoogleCallback(@RequestParam("access_token") String accessToken) { + // A validação pode ser melhorada com Bean Validation (@Valid) no DTO if (accessToken == null || accessToken.isBlank()) { return ResponseEntity - .status(HttpStatus.UNAUTHORIZED) // Use HttpStatus para clareza - .body("{\"error\": \"Access token ausente.\"}"); + .status(HttpStatus.BAD_REQUEST) // BAD_REQUEST é mais apropriado para input inválido + .body(new ErrorResponse("O access token é obrigatório.")); } - return authService.getUserInfo(accessToken); + // Delega a lógica para o serviço, que já retorna uma ResponseEntity + return authService.loginOrRegisterWithGoogle(accessToken); } } diff --git a/src/main/java/com/api/apiviagem/controller/RoleController.java b/src/main/java/com/api/apiviagem/controller/RoleController.java new file mode 100644 index 0000000..d00318d --- /dev/null +++ b/src/main/java/com/api/apiviagem/controller/RoleController.java @@ -0,0 +1,122 @@ +package com.api.apiviagem.controller; + +import com.api.apiviagem.DTO.request.RoleRequestDTO; +import com.api.apiviagem.DTO.response.RoleResponseDTO; +import com.api.apiviagem.exception.ResourceNotFoundException; +import com.api.apiviagem.model.Role; +import com.api.apiviagem.model.RoleType; +import com.api.apiviagem.service.RoleService; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/v1/roles") +@CrossOrigin(origins = "*") +public class RoleController { + + @Autowired + private RoleService roleService; + + /** + * Busca todas as roles cadastradas no sistema. + */ + @GetMapping + public ResponseEntity> getAllRoles() { + List roles = roleService.getAllRoles(); + List responseDTOs = roles.stream() + .map(RoleResponseDTO::fromRole) + .collect(Collectors.toList()); + return ResponseEntity.ok(responseDTOs); + } + + /** + * Busca uma role pelo seu ID. + */ + @GetMapping("/{id}") + public ResponseEntity getRoleById(@PathVariable UUID id) { + try { + Role role = roleService.getRoleById(id); + RoleResponseDTO responseDTO = RoleResponseDTO.fromRole(role); + return ResponseEntity.ok(responseDTO); + } catch (ResourceNotFoundException e) { + return ResponseEntity.notFound().build(); + } + } + + /** + * Busca uma role pelo seu tipo. + */ + @GetMapping("/name/{name}") + public ResponseEntity getRoleByName(@PathVariable RoleType name) { + try { + Role role = roleService.getRoleByName(name); + RoleResponseDTO responseDTO = RoleResponseDTO.fromRole(role); + return ResponseEntity.ok(responseDTO); + } catch (ResourceNotFoundException e) { + return ResponseEntity.notFound().build(); + } + } + + /** + * Cadastra uma nova role no sistema. + */ + @PostMapping + public ResponseEntity createRole(@Valid @RequestBody RoleRequestDTO requestDTO) { + try { + Role role = roleService.createRole(requestDTO.name(), requestDTO.description()); + RoleResponseDTO responseDTO = RoleResponseDTO.fromRole(role); + return ResponseEntity.status(HttpStatus.CREATED).body(responseDTO); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().build(); + } + } + + /** + * Atualiza uma role existente. + */ + @PutMapping("/{id}") + public ResponseEntity updateRole( + @PathVariable UUID id, + @RequestBody RoleRequestDTO requestDTO) { + try { + Role role = roleService.updateRole(id, requestDTO.description()); + RoleResponseDTO responseDTO = RoleResponseDTO.fromRole(role); + return ResponseEntity.ok(responseDTO); + } catch (ResourceNotFoundException e) { + return ResponseEntity.notFound().build(); + } + } + + /** + * Deleta uma role pelo seu ID. + */ + @DeleteMapping("/{id}") + public ResponseEntity deleteRoleById(@PathVariable UUID id) { + try { + roleService.deleteRoleById(id); + return ResponseEntity.noContent().build(); + } catch (ResourceNotFoundException e) { + return ResponseEntity.notFound().build(); + } + } + + /** + * Deleta uma role pelo seu tipo. + */ + @DeleteMapping("/name/{name}") + public ResponseEntity deleteRoleByName(@PathVariable RoleType name) { + try { + roleService.deleteRoleByName(name); + return ResponseEntity.noContent().build(); + } catch (ResourceNotFoundException e) { + return ResponseEntity.notFound().build(); + } + } +} diff --git a/src/main/java/com/api/apiviagem/controller/UserController.java b/src/main/java/com/api/apiviagem/controller/UserController.java new file mode 100644 index 0000000..e581809 --- /dev/null +++ b/src/main/java/com/api/apiviagem/controller/UserController.java @@ -0,0 +1,131 @@ +package com.api.apiviagem.controller; + +import com.api.apiviagem.DTO.response.RoleResponseDTO; +import com.api.apiviagem.exception.ResourceNotFoundException; +import com.api.apiviagem.model.RoleType; +import com.api.apiviagem.model.User; +import com.api.apiviagem.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/v1/users") +@CrossOrigin(origins = "*") +public class UserController { + + @Autowired + private UserService userService; + + /** + * Atribui uma role a um usuário. + */ + @PostMapping("/{userId}/roles/{roleType}") + public ResponseEntity assignRoleToUser( + @PathVariable UUID userId, + @PathVariable RoleType roleType) { + try { + User user = userService.assignRoleToUser(userId, roleType); + return ResponseEntity.ok(user); + } catch (ResourceNotFoundException e) { + return ResponseEntity.notFound().build(); + } + } + + /** + * Remove uma role de um usuário. + */ + @DeleteMapping("/{userId}/roles/{roleType}") + public ResponseEntity removeRoleFromUser( + @PathVariable UUID userId, + @PathVariable RoleType roleType) { + try { + User user = userService.removeRoleFromUser(userId, roleType); + return ResponseEntity.ok(user); + } catch (ResourceNotFoundException e) { + return ResponseEntity.notFound().build(); + } + } + + /** + * Busca usuários por tipo de role. + */ + @GetMapping("/by-role/{roleType}") + public ResponseEntity> getUsersByRole(@PathVariable RoleType roleType) { + List users = userService.getUsersByRole(roleType); + return ResponseEntity.ok(users); + } + + /** + * Busca todos os usuários. + */ + @GetMapping + public ResponseEntity> getAllUsers() { + return ResponseEntity.ok().body(userService.getAllUsers()); + } + + /** + * Busca todos os usuários administradores. + */ + @GetMapping("/admins") + public ResponseEntity> getAdminUsers() { + List adminUsers = userService.getAdminUsers(); + return ResponseEntity.ok(adminUsers); + } + + /** + * Busca todos os usuários regulares. + */ + @GetMapping("/regular-users") + public ResponseEntity> getRegularUsers() { + List regularUsers = userService.getRegularUsers(); + return ResponseEntity.ok(regularUsers); + } + + /** + * Verifica se um usuário possui uma role específica. + */ + @GetMapping("/{userId}/has-role/{roleType}") + public ResponseEntity userHasRole( + @PathVariable UUID userId, + @PathVariable RoleType roleType) { + boolean hasRole = userService.userHasRole(userId, roleType); + return ResponseEntity.ok(hasRole); + } + + /** + * Define as roles de um usuário (substitui todas as roles existentes). + */ + @PutMapping("/{userId}/roles") + public ResponseEntity setUserRoles( + @PathVariable UUID userId, + @RequestBody Set roleTypes) { + try { + User user = userService.setUserRoles(userId, roleTypes); + return ResponseEntity.ok(user); + } catch (ResourceNotFoundException e) { + return ResponseEntity.notFound().build(); + } + } + + /** + * Busca as roles de um usuário específico. + */ + @GetMapping("/{userId}/roles") + public ResponseEntity> getUserRoles(@PathVariable UUID userId) { + try { + User user = userService.getUserById(userId); + List roles = user.getRoles().stream() + .map(RoleResponseDTO::fromRole) + .collect(Collectors.toList()); + return ResponseEntity.ok(roles); + } catch (ResourceNotFoundException e) { + return ResponseEntity.notFound().build(); + } + } +} diff --git a/src/main/java/com/api/apiviagem/model/Role.java b/src/main/java/com/api/apiviagem/model/Role.java new file mode 100644 index 0000000..80cbf54 --- /dev/null +++ b/src/main/java/com/api/apiviagem/model/Role.java @@ -0,0 +1,91 @@ +package com.api.apiviagem.model; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.Instant; +import java.util.UUID; + +@Entity +@Table(name = "roles") +public class Role { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, unique = true) + private RoleType name; + + @Column(length = 255) + private String description; + + @CreationTimestamp + @Column(name = "created_at", updatable = false, nullable = false) + private Instant createdAt; + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private Instant updatedAt; + + public Role() { + } + + public Role(RoleType name, String description) { + this.name = name; + this.description = description; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public RoleType getName() { + return name; + } + + public void setName(RoleType name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public String toString() { + return "Role{" + + "id=" + id + + ", name=" + name + + ", description='" + description + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/src/main/java/com/api/apiviagem/model/RoleType.java b/src/main/java/com/api/apiviagem/model/RoleType.java new file mode 100644 index 0000000..093bdc0 --- /dev/null +++ b/src/main/java/com/api/apiviagem/model/RoleType.java @@ -0,0 +1,24 @@ +package com.api.apiviagem.model; + +/** + * Enum que define os tipos de roles disponíveis no sistema. + */ +public enum RoleType { + USER("Usuário comum do sistema"), + ADMIN("Administrador com acesso total ao sistema"); + + private final String description; + + RoleType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + + @Override + public String toString() { + return this.name(); + } +} diff --git a/src/main/java/com/api/apiviagem/model/User.java b/src/main/java/com/api/apiviagem/model/User.java index d084e94..53aafc9 100644 --- a/src/main/java/com/api/apiviagem/model/User.java +++ b/src/main/java/com/api/apiviagem/model/User.java @@ -8,6 +8,7 @@ import org.hibernate.annotations.UpdateTimestamp; import java.time.Instant; +import java.util.Set; import java.util.UUID; @DataAmount // Anotação do Lombok: gera getters, setters, toString, equals, hashCode @@ -52,10 +53,20 @@ public class User { @Column(name = "auth_provider", nullable = false) private AuthProvider authProvider; + // --- Controle de Roles --- + + @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable( + name = "user_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id") + ) + private Set roles; + public User() { } - public User(UUID id, String email, String name, Instant createdAt, Instant updatedAt, String googleId, String imageUrl, AuthProvider authProvider) { + public User(UUID id, String email, String name, Instant createdAt, Instant updatedAt, String googleId, String imageUrl, AuthProvider authProvider, Set roles) { this.id = id; this.email = email; this.name = name; @@ -64,6 +75,7 @@ public User(UUID id, String email, String name, Instant createdAt, Instant updat this.googleId = googleId; this.imageUrl = imageUrl; this.authProvider = authProvider; + this.roles = roles; } public UUID getId() { @@ -129,4 +141,34 @@ public AuthProvider getAuthProvider() { public void setAuthProvider(AuthProvider authProvider) { this.authProvider = authProvider; } + + public Set getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } + + // Métodos utilitários para gerenciar roles + public void addRole(Role role) { + if (this.roles == null) { + this.roles = new java.util.HashSet<>(); + } + this.roles.add(role); + } + + public void removeRole(Role role) { + if (this.roles != null) { + this.roles.remove(role); + } + } + + public boolean hasRole(RoleType roleType) { + if (this.roles == null) { + return false; + } + return this.roles.stream() + .anyMatch(role -> role.getName().equals(roleType)); + } } diff --git a/src/main/java/com/api/apiviagem/repository/RoleRepository.java b/src/main/java/com/api/apiviagem/repository/RoleRepository.java new file mode 100644 index 0000000..872b985 --- /dev/null +++ b/src/main/java/com/api/apiviagem/repository/RoleRepository.java @@ -0,0 +1,28 @@ +package com.api.apiviagem.repository; + +import com.api.apiviagem.model.Role; +import com.api.apiviagem.model.RoleType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface RoleRepository extends JpaRepository { + + /** + * Busca uma role pelo seu tipo. + */ + Optional findByName(RoleType name); + + /** + * Verifica se uma role com o tipo especificado existe. + */ + boolean existsByName(RoleType name); + + /** + * Deleta uma role pelo seu tipo. + */ + void deleteByName(RoleType name); +} diff --git a/src/main/java/com/api/apiviagem/repository/UserRepository.java b/src/main/java/com/api/apiviagem/repository/UserRepository.java index e21ab0a..835236a 100644 --- a/src/main/java/com/api/apiviagem/repository/UserRepository.java +++ b/src/main/java/com/api/apiviagem/repository/UserRepository.java @@ -1,9 +1,13 @@ package com.api.apiviagem.repository; +import com.api.apiviagem.model.RoleType; import com.api.apiviagem.model.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -23,4 +27,30 @@ public interface UserRepository extends JpaRepository { */ Optional findByGoogleId(String googleId); + // --- Métodos relacionados a Roles --- + + /** + * Busca usuários que possuem uma role específica. + */ + @Query("SELECT DISTINCT u FROM User u JOIN u.roles r WHERE r.name = :roleType") + List findByRoleType(@Param("roleType") RoleType roleType); + + /** + * Busca usuários que são administradores. + */ + @Query("SELECT DISTINCT u FROM User u JOIN u.roles r WHERE r.name = 'ADMIN'") + List findAdminUsers(); + + /** + * Busca usuários que são usuários comuns. + */ + @Query("SELECT DISTINCT u FROM User u JOIN u.roles r WHERE r.name = 'USER'") + List findRegularUsers(); + + /** + * Verifica se um usuário possui uma role específica. + */ + @Query("SELECT CASE WHEN COUNT(u) > 0 THEN true ELSE false END FROM User u JOIN u.roles r WHERE u.id = :userId AND r.name = :roleType") + boolean userHasRole(@Param("userId") UUID userId, @Param("roleType") RoleType roleType); + } diff --git a/src/main/java/com/api/apiviagem/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/api/apiviagem/security/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..a0d1e14 --- /dev/null +++ b/src/main/java/com/api/apiviagem/security/JwtAuthenticationEntryPoint.java @@ -0,0 +1,46 @@ +package com.api.apiviagem.security; + +import com.api.apiviagem.DTO.response.ApiErrorResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.time.Instant; + +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + + // Injeção de dependência do ObjectMapper + public JwtAuthenticationEntryPoint(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + + // Headers CORS foram removidos. O filtro principal já lida com isso. + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + ApiErrorResponse errorResponse = new ApiErrorResponse( + HttpServletResponse.SC_UNAUTHORIZED, + "Não Autorizado", + "Autenticação necessária para acessar este recurso.", // Mensagem genérica e segura + Instant.now() + ); + + // Escreve a resposta JSON de forma segura + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } +} diff --git a/src/main/java/com/api/apiviagem/security/JwtAuthenticationFilter.java b/src/main/java/com/api/apiviagem/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..b33f3a6 --- /dev/null +++ b/src/main/java/com/api/apiviagem/security/JwtAuthenticationFilter.java @@ -0,0 +1,65 @@ +package com.api.apiviagem.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private JwtTokenProvider tokenProvider; + + @Autowired + private UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String token = getTokenFromRequest(request); + + if(StringUtils.hasText(token) && tokenProvider.validateToken(token)) { + String username = tokenProvider.getUsername(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + + filterChain.doFilter(request, response); + } + + private String getTokenFromRequest(HttpServletRequest request){ + // First, try to get token from Authorization header + String bearerToken = request.getHeader("Authorization"); + if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")){ + return bearerToken.substring(7); + } + + // If not found in header, try to get from cookies + Cookie[] cookies = request.getCookies(); + if(cookies != null) { + for(Cookie cookie : cookies) { + if("jwtToken".equals(cookie.getName())) { + return cookie.getValue(); + } + } + } + + return null; + } +} + diff --git a/src/main/java/com/api/apiviagem/security/JwtTokenProvider.java b/src/main/java/com/api/apiviagem/security/JwtTokenProvider.java new file mode 100644 index 0000000..ff83c28 --- /dev/null +++ b/src/main/java/com/api/apiviagem/security/JwtTokenProvider.java @@ -0,0 +1,85 @@ +package com.api.apiviagem.security; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.security.Key; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class JwtTokenProvider { + + private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class); + + @Value("${jwt.secret}") + private String jwtSecret; + + @Value("${jwt.duration}") + private long jwtExpirationDate; + + public String generateToken(Authentication authentication) { + String username = authentication.getName(); + Date currentDate = new Date(); + Date expirationDate = new Date(currentDate.getTime() + jwtExpirationDate); + + // Extraindo as roles (authorities) do usuário para incluir no token + List roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + + return Jwts.builder() + .subject(username) + .claim("roles", roles) // <-- ADICIONADO: Incluindo roles como custom claim + .issuedAt(currentDate) + .expiration(expirationDate) + .signWith(key()) + .compact(); + } + + private Key key() { + return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret)); + } + + public String getUsername(String token) { + Claims claims = Jwts.parser() + .verifyWith((SecretKey) key()) + .build() + .parseSignedClaims(token) + .getPayload(); + return claims.getSubject(); + } + + public boolean validateToken(String token) { + try { + Jwts.parser() + .verifyWith((SecretKey) key()) + .build() + .parse(token); + return true; + // Capturando as exceções específicas e logando a causa da falha + } catch (MalformedJwtException e) { + logger.error("Token JWT malformado: {}", e.getMessage()); + } catch (ExpiredJwtException e) { + logger.warn("Token JWT expirado: {}", e.getMessage()); + } catch (UnsupportedJwtException e) { + logger.error("Token JWT não suportado: {}", e.getMessage()); + } catch (IllegalArgumentException e) { + logger.error("Payload do token JWT está vazio ou nulo: {}", e.getMessage()); + } catch (SignatureException e) { + logger.error("Assinatura do token JWT inválida: {}", e.getMessage()); + } + // Se qualquer exceção for capturada, a validação falha + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/api/apiviagem/security/SecurityConfig.java b/src/main/java/com/api/apiviagem/security/SecurityConfig.java new file mode 100644 index 0000000..2fc1c42 --- /dev/null +++ b/src/main/java/com/api/apiviagem/security/SecurityConfig.java @@ -0,0 +1,92 @@ +package com.api.apiviagem.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; // <-- IMPORT ADICIONADO +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfig { + + @Autowired + private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public PasswordEncoder getPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of("http://localhost:3000")); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With", "accept", "Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers")); + configuration.setAllowCredentials(true); + configuration.setExposedHeaders(List.of("Content-Disposition", "Authorization")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .headers(headers -> headers + .httpStrictTransportSecurity(hsts -> hsts.includeSubDomains(true).maxAgeInSeconds(31536000)) // Força HTTPS (HSTS) + .frameOptions(frameOptions -> frameOptions.deny()) // Previne Clickjacking + ) + .csrf(csrf -> csrf.disable()) // Desabilita CSRF para API stateless + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/api/v1/users/**").hasAuthority("ROLE_ADM") + .requestMatchers("/api/v1/roles/**").hasAuthority("ROLE_ADM") + // Agrupando endpoints de autenticação e permitindo acesso público + .requestMatchers("/api/v1/auth/**").permitAll() + .requestMatchers("/api/v1/city/**").permitAll() + .requestMatchers("/api/v1/holiday/**").permitAll() + .requestMatchers("/api/v1/low-budget/**").permitAll() + .requestMatchers("/send-email/**").permitAll() + .requestMatchers("/api/v1/travel-style/**").permitAll() + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + .anyRequest().authenticated() + ) + .sessionManagement(session -> session + // 1. MUDANÇA CRÍTICA: Garante que a aplicação é stateless + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(jwtAuthenticationEntryPoint) + ); + + http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/api/apiviagem/service/AuthService.java b/src/main/java/com/api/apiviagem/service/AuthService.java index f1917ce..7911fcd 100644 --- a/src/main/java/com/api/apiviagem/service/AuthService.java +++ b/src/main/java/com/api/apiviagem/service/AuthService.java @@ -1,73 +1,123 @@ package com.api.apiviagem.service; +import com.api.apiviagem.DTO.response.UserResponseDTO; import com.api.apiviagem.model.AuthProvider; import com.api.apiviagem.model.GetInfosGoogle; import com.api.apiviagem.model.User; import com.api.apiviagem.repository.UserRepository; +import com.api.apiviagem.security.JwtTokenProvider; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; +import org.springframework.http.ResponseCookie; // Import necessário -import java.util.List; -import java.util.Optional; +import java.util.Collections; @Service public class AuthService { + @Autowired private UserRepository userRepository; - private static final String GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"; + @Autowired + private UserService userService; - public List getUsers() { - return userRepository.findAll(); - } + @Autowired + private JwtTokenProvider jwtTokenProvider; - public ResponseEntity getUserInfo(String accessToken) { - try{ - RestTemplate restTemplate = new RestTemplate(); + // Removido o AuthenticationManager, ele não é usado aqui. - // Montando o Header com o Bearer Token - HttpHeaders headers = new HttpHeaders(); - headers.setBearerAuth(accessToken); // equivale a "Authorization: Bearer " + private static final String GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"; + + public ResponseEntity loginOrRegisterWithGoogle(String googleAccessToken) { + try { + // 1. Obter informações do usuário do Google + GetInfosGoogle googleUserInfo = getUserInfoFromGoogle(googleAccessToken); - HttpEntity entity = new HttpEntity<>(headers); + // 2. Encontrar ou criar o usuário no nosso banco de dados + User user = findOrCreateUser(googleUserInfo); - ResponseEntity response = restTemplate.exchange( - GOOGLE_USERINFO_URL, - HttpMethod.GET, - entity, - GetInfosGoogle.class // 👈 mapeia direto para objeto + // 3. Criar a autenticação manualmente (identidade já verificada pelo Google) + // Aqui, usamos o email como principal e null para credenciais, pois não há senha. + // As roles/authorities devem ser carregadas do nosso banco. + Authentication authentication = new UsernamePasswordAuthenticationToken( + user.getEmail(), + null, + Collections.singletonList(new SimpleGrantedAuthority(user.getRoles().toString())) // Ou as roles reais do usuário ); - GetInfosGoogle infos = response.getBody(); - if (infos == null) { - return ResponseEntity.badRequest().body("Não foi possivel fazer login"); - } - - User user = userRepository.existsByEmail(infos.getEmail()) - ? userRepository.findAll() - .stream() - .filter(u -> u.getEmail().equals(infos.getEmail())) - .findFirst() - .orElse(new User()) - : new User(); - - // Mapeia os dados do Google para a entidade User - user.setGoogleId(infos.getSub()); - user.setName(infos.getName()); - user.setEmail(infos.getEmail()); - user.setImageUrl(infos.getPicture()); - user.setAuthProvider(AuthProvider.GOOGLE); - userRepository.save(user); - // Salva no banco - return ResponseEntity.ok().body("Login Feito com Sucesso"); - } catch (Exception e){ - return ResponseEntity.badRequest().body("Não foi possivel fazer login"); + // Define o principal autenticado no contexto de segurança do Spring + SecurityContextHolder.getContext().setAuthentication(authentication); + + // 4. Gerar nosso JWT interno para o usuário + String localJwt = jwtTokenProvider.generateToken(authentication); + + // 5. Criar o cookie e retorná-lo na resposta + ResponseCookie cookie = ResponseCookie.from("jwtToken", localJwt) + .httpOnly(true) + .secure(true) // Use 'false' apenas em ambiente de desenvolvimento com HTTP + .path("/") + .maxAge(7 * 24 * 60 * 60) // 7 dias + .build(); + + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, cookie.toString()) + .body(new UserResponseDTO(user.getId(), user.getName(), + user.getEmail(), user.getImageUrl(), + user.getRoles(), user.getCreatedAt(), + user.getUpdatedAt())); + + } catch (HttpClientErrorException.Unauthorized e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Token do Google inválido ou expirado."); + } catch (Exception e) { + // Logar o erro real aqui é importante para depuração + // logger.error("Erro inesperado durante o login com Google: ", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Ocorreu um erro no servidor."); } + } + + private GetInfosGoogle getUserInfoFromGoogle(String accessToken) { + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange( + GOOGLE_USERINFO_URL, + HttpMethod.GET, + entity, + GetInfosGoogle.class + ); + + if (response.getBody() == null) { + throw new IllegalStateException("Não foi possível obter informações do usuário do Google."); + } + return response.getBody(); + } + + private User findOrCreateUser(GetInfosGoogle googleInfo) { + // Busca o usuário pelo e-mail de forma eficiente + return userRepository.findByEmail(googleInfo.getEmail()) + .orElseGet(() -> { + // Se não encontrar, cria um novo + User newUser = new User(); + newUser.setGoogleId(googleInfo.getSub()); + newUser.setName(googleInfo.getName()); + newUser.setEmail(googleInfo.getEmail()); + newUser.setImageUrl(googleInfo.getPicture()); + newUser.setAuthProvider(AuthProvider.GOOGLE); + + User savedUser = userRepository.save(newUser); + // Atribui a role padrão ao novo usuário + userService.assignDefaultRole(savedUser); + return savedUser; + }); } -} +} \ No newline at end of file diff --git a/src/main/java/com/api/apiviagem/service/CustomUserDetailsService.java b/src/main/java/com/api/apiviagem/service/CustomUserDetailsService.java new file mode 100644 index 0000000..0779509 --- /dev/null +++ b/src/main/java/com/api/apiviagem/service/CustomUserDetailsService.java @@ -0,0 +1,56 @@ +package com.api.apiviagem.service; + +import com.api.apiviagem.model.User; +import com.api.apiviagem.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +@Service // 👈 Anotação crucial para que o Spring reconheça esta classe como um Bean +public class CustomUserDetailsService implements UserDetailsService { + + @Autowired + private UserRepository userRepository; + + /** + * Este método é chamado pelo Spring Security quando ele precisa carregar + * os dados de um usuário para realizar a autenticação. + * @param username O nome de usuário (no nosso caso, o email) que está tentando se autenticar. + * @return um objeto UserDetails contendo as informações do usuário. + * @throws UsernameNotFoundException se o usuário não for encontrado no banco de dados. + */ + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 1. Busca o usuário no repositório pelo email + User user = userRepository.findByEmail(username) + .orElseThrow(() -> + new UsernameNotFoundException("Usuário não encontrado com o email: " + username)); + + // 2. Converte as roles (permissões) do seu usuário para o formato que o Spring Security entende + // (uma coleção de GrantedAuthority). + // Estou assumindo que sua entidade User tem um método getRoles() que retorna um Set + // e que a entidade Role tem um método getName() que retorna a string da role (ex: "ROLE_ADM"). + Set authorities = user.getRoles() + .stream() + .map(role -> new SimpleGrantedAuthority(role.getName().name())) + .collect(Collectors.toSet()); + + String password = ""; + + // 3. Retorna um objeto User do próprio Spring Security, que implementa UserDetails. + // Este objeto contém o email, a senha (hash) e as permissões do usuário. + return new org.springframework.security.core.userdetails.User( + user.getEmail(), + password, // O Spring Security precisa da senha para o fluxo padrão de login + authorities + ); + } +} diff --git a/src/main/java/com/api/apiviagem/service/RoleService.java b/src/main/java/com/api/apiviagem/service/RoleService.java new file mode 100644 index 0000000..d2428cc --- /dev/null +++ b/src/main/java/com/api/apiviagem/service/RoleService.java @@ -0,0 +1,91 @@ +package com.api.apiviagem.service; + +import com.api.apiviagem.exception.ResourceNotFoundException; +import com.api.apiviagem.model.Role; +import com.api.apiviagem.model.RoleType; +import com.api.apiviagem.repository.RoleRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Service +public class RoleService { + + @Autowired + private RoleRepository roleRepository; + + /** + * Busca todas as roles cadastradas no sistema. + */ + public List getAllRoles() { + return roleRepository.findAll(); + } + + /** + * Busca uma role pelo seu ID. + */ + public Role getRoleById(UUID id) { + return roleRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Role não encontrada com ID: " + id)); + } + + /** + * Busca uma role pelo seu tipo. + */ + public Role getRoleByName(RoleType name) { + return roleRepository.findByName(name) + .orElseThrow(() -> new ResourceNotFoundException("Role não encontrada com nome: " + name)); + } + + /** + * Cadastra uma nova role no sistema. + */ + public Role createRole(RoleType roleType, String description) { + // Verifica se a role já existe + if (roleRepository.existsByName(roleType)) { + throw new IllegalArgumentException("Role já existe com o tipo: " + roleType); + } + + Role role = new Role(roleType, description); + return roleRepository.save(role); + } + + /** + * Atualiza uma role existente. + */ + public Role updateRole(UUID id, String description) { + Role role = getRoleById(id); + role.setDescription(description); + return roleRepository.save(role); + } + + /** + * Deleta uma role pelo seu ID. + */ + public void deleteRoleById(UUID id) { + if (!roleRepository.existsById(id)) { + throw new ResourceNotFoundException("Role não encontrada com ID: " + id); + } + roleRepository.deleteById(id); + } + + /** + * Deleta uma role pelo seu tipo. + */ + public void deleteRoleByName(RoleType name) { + if (!roleRepository.existsByName(name)) { + throw new ResourceNotFoundException("Role não encontrada com nome: " + name); + } + roleRepository.deleteByName(name); + } + + /** + * Verifica se uma role existe pelo seu tipo. + */ + public boolean roleExists(RoleType name) { + return roleRepository.existsByName(name); + } +} diff --git a/src/main/java/com/api/apiviagem/service/UserService.java b/src/main/java/com/api/apiviagem/service/UserService.java new file mode 100644 index 0000000..07081db --- /dev/null +++ b/src/main/java/com/api/apiviagem/service/UserService.java @@ -0,0 +1,126 @@ +package com.api.apiviagem.service; + +import com.api.apiviagem.exception.ResourceNotFoundException; +import com.api.apiviagem.model.Role; +import com.api.apiviagem.model.RoleType; +import com.api.apiviagem.model.User; +import com.api.apiviagem.repository.RoleRepository; +import com.api.apiviagem.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Set; +import java.util.UUID; + +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoleRepository roleRepository; + + /** + * Atribui uma role a um usuário. + */ + public User assignRoleToUser(UUID userId, RoleType roleType) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("Usuário não encontrado com ID: " + userId)); + + Role role = roleRepository.findByName(roleType) + .orElseThrow(() -> new ResourceNotFoundException("Role não encontrada: " + roleType)); + + user.addRole(role); + return userRepository.save(user); + } + + /** + * Remove uma role de um usuário. + */ + public User removeRoleFromUser(UUID userId, RoleType roleType) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("Usuário não encontrado com ID: " + userId)); + + Role role = roleRepository.findByName(roleType) + .orElseThrow(() -> new ResourceNotFoundException("Role não encontrada: " + roleType)); + + user.removeRole(role); + return userRepository.save(user); + } + + /** + * Busca usuários por tipo de role. + */ + public List getUsersByRole(RoleType roleType) { + return userRepository.findByRoleType(roleType); + } + + /** + * Busca todos os usuários. + */ + public List getAllUsers() { + return userRepository.findAll(); + } + + /** + * Busca todos os usuários administradores. + */ + public List getAdminUsers() { + return userRepository.findAdminUsers(); + } + + /** + * Busca todos os usuários regulares. + */ + public List getRegularUsers() { + return userRepository.findRegularUsers(); + } + + /** + * Verifica se um usuário possui uma role específica. + */ + public boolean userHasRole(UUID userId, RoleType roleType) { + return userRepository.userHasRole(userId, roleType); + } + + /** + * Define as roles de um usuário (substitui todas as roles existentes). + */ + public User setUserRoles(UUID userId, Set roleTypes) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("Usuário não encontrado com ID: " + userId)); + + // Limpa as roles existentes + user.setRoles(new java.util.HashSet<>()); + + // Adiciona as novas roles + for (RoleType roleType : roleTypes) { + Role role = roleRepository.findByName(roleType) + .orElseThrow(() -> new ResourceNotFoundException("Role não encontrada: " + roleType)); + user.addRole(role); + } + + return userRepository.save(user); + } + + /** + * Atribui a role padrão USER para novos usuários. + */ + public User assignDefaultRole(User user) { + Role defaultRole = roleRepository.findByName(RoleType.USER) + .orElseThrow(() -> new ResourceNotFoundException("Role USER não encontrada")); + + user.addRole(defaultRole); + return userRepository.save(user); + } + + /** + * Busca um usuário pelo ID. + */ + public User getUserById(UUID userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("Usuário não encontrado com ID: " + userId)); + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 4cf91c2..c9cf81c 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -12,4 +12,6 @@ spring: hibernate: ddl-auto: update - +admin: + name: Administrador + email: destinyfyone@gmail.com diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b9fa2c8..67f0975 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -7,3 +7,6 @@ spring: main: banner-mode: off +jwt: + secret: ${JWT_SECRET} + duration: ${JWT_DURATION}