feat: otimização de performance e ajustes finais
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>br.gov.sigefp</groupId>
|
||||
<artifactId>sigefp-parent</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>sigefp-admin</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>SIGEFP Admin</name>
|
||||
<description>Módulo de administração: utilizadores, perfis, auditoria</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>br.gov.sigefp</groupId>
|
||||
<artifactId>sigefp-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package br.gov.sigefp.admin.api;
|
||||
|
||||
import br.gov.sigefp.admin.api.dto.AuditLogDTO;
|
||||
import br.gov.sigefp.admin.service.AuditLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Controller REST para consulta de logs de auditoria (somente leitura).
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/audit-logs")
|
||||
@RequiredArgsConstructor
|
||||
public class AuditLogController {
|
||||
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<AuditLogDTO>> findLogs(
|
||||
@RequestParam(required = false) UUID userId,
|
||||
@RequestParam(required = false) String module,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant startDate,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant endDate,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size,
|
||||
@RequestParam(required = false) String sortBy,
|
||||
@RequestParam(required = false, defaultValue = "DESC") String sortDirection) {
|
||||
|
||||
Sort sort = sortBy != null
|
||||
? Sort.by(Sort.Direction.fromString(sortDirection), sortBy)
|
||||
: Sort.by(Sort.Direction.DESC, "createdAt");
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
Page<AuditLogDTO> result = auditLogService.findLogs(userId, module, startDate, endDate, pageable);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package br.gov.sigefp.admin.api;
|
||||
|
||||
import br.gov.sigefp.admin.api.dto.RoleDTO;
|
||||
import br.gov.sigefp.admin.service.RoleService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Controller REST para gestão de perfis/roles.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/roles")
|
||||
@RequiredArgsConstructor
|
||||
public class RoleController {
|
||||
|
||||
private final RoleService roleService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> findAll(
|
||||
@RequestParam(value = "page", defaultValue = "0") int page,
|
||||
@RequestParam(value = "size", defaultValue = "20") int size,
|
||||
@RequestParam(value = "sortBy", required = false) String sortBy,
|
||||
@RequestParam(value = "sortDirection", required = false, defaultValue = "ASC") String sortDirection) {
|
||||
try {
|
||||
Sort sort = sortBy != null && !sortBy.isEmpty()
|
||||
? Sort.by(Sort.Direction.fromString(sortDirection), sortBy)
|
||||
: Sort.by(Sort.Direction.ASC, "code");
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
Page<RoleDTO> result = roleService.findAll(pageable);
|
||||
return ResponseEntity.ok(result);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("Erro ao buscar perfis: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<RoleDTO> findById(@PathVariable("id") UUID id) {
|
||||
try {
|
||||
RoleDTO dto = roleService.findById(id);
|
||||
return ResponseEntity.ok(dto);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<RoleDTO> create(@Valid @RequestBody RoleDTO dto) {
|
||||
try {
|
||||
RoleDTO created = roleService.create(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<RoleDTO> update(
|
||||
@PathVariable("id") UUID id,
|
||||
@Valid @RequestBody RoleDTO dto) {
|
||||
try {
|
||||
RoleDTO updated = roleService.update(id, dto);
|
||||
return ResponseEntity.ok(updated);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package br.gov.sigefp.admin.api;
|
||||
|
||||
import br.gov.sigefp.admin.api.dto.AssignRolesDTO;
|
||||
import br.gov.sigefp.admin.api.dto.UserAccountDTO;
|
||||
import br.gov.sigefp.admin.service.UserService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Controller REST para gestão de utilizadores.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/users")
|
||||
@RequiredArgsConstructor
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> findAll(
|
||||
@RequestParam(value = "page", defaultValue = "0") int page,
|
||||
@RequestParam(value = "size", defaultValue = "20") int size,
|
||||
@RequestParam(value = "sortBy", required = false) String sortBy,
|
||||
@RequestParam(value = "sortDirection", required = false, defaultValue = "ASC") String sortDirection) {
|
||||
try {
|
||||
Sort sort = sortBy != null && !sortBy.isEmpty()
|
||||
? Sort.by(Sort.Direction.fromString(sortDirection), sortBy)
|
||||
: Sort.by(Sort.Direction.ASC, "createdAt");
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
Page<UserAccountDTO> result = userService.findAll(pageable);
|
||||
return ResponseEntity.ok(result);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("Erro ao buscar utilizadores: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<UserAccountDTO> findById(@PathVariable("id") UUID id) {
|
||||
try {
|
||||
UserAccountDTO dto = userService.findById(id);
|
||||
return ResponseEntity.ok(dto);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<UserAccountDTO> create(@Valid @RequestBody UserAccountDTO dto) {
|
||||
try {
|
||||
UserAccountDTO created = userService.create(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<UserAccountDTO> update(
|
||||
@PathVariable("id") UUID id,
|
||||
@Valid @RequestBody UserAccountDTO dto) {
|
||||
try {
|
||||
UserAccountDTO updated = userService.update(id, dto);
|
||||
return ResponseEntity.ok(updated);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/roles")
|
||||
public ResponseEntity<Void> assignRoles(
|
||||
@PathVariable("id") UUID id,
|
||||
@Valid @RequestBody AssignRolesDTO dto) {
|
||||
try {
|
||||
userService.assignRoles(id, dto.getRoleIds());
|
||||
return ResponseEntity.ok().build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package br.gov.sigefp.admin.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para atribuição de perfis a um utilizador.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AssignRolesDTO {
|
||||
|
||||
@NotEmpty(message = "Lista de IDs de perfis não pode estar vazia")
|
||||
private List<UUID> roleIds;
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package br.gov.sigefp.admin.api.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de log de auditoria.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AuditLogDTO {
|
||||
|
||||
private UUID id;
|
||||
|
||||
private UUID userId;
|
||||
|
||||
private String userUsername; // Nome do usuário para facilitar visualização
|
||||
|
||||
private String module;
|
||||
|
||||
private String action;
|
||||
|
||||
private String entity;
|
||||
|
||||
private UUID entityId;
|
||||
|
||||
private String description;
|
||||
|
||||
private Instant createdAt;
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package br.gov.sigefp.admin.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de perfil/role.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RoleDTO {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "Código é obrigatório")
|
||||
@Size(min = 3, max = 50, message = "Código deve ter entre 3 e 50 caracteres")
|
||||
private String code;
|
||||
|
||||
@NotBlank(message = "Nome é obrigatório")
|
||||
@Size(max = 200, message = "Nome deve ter no máximo 200 caracteres")
|
||||
private String name;
|
||||
|
||||
@Size(max = 500, message = "Descrição deve ter no máximo 500 caracteres")
|
||||
private String description;
|
||||
}
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package br.gov.sigefp.admin.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de conta de utilizador.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserAccountDTO {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "Username é obrigatório")
|
||||
@Size(min = 3, max = 50, message = "Username deve ter entre 3 e 50 caracteres")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "Nome completo é obrigatório")
|
||||
@Size(max = 200, message = "Nome completo deve ter no máximo 200 caracteres")
|
||||
private String fullName;
|
||||
|
||||
@NotBlank(message = "Email é obrigatório")
|
||||
@Email(message = "Email inválido")
|
||||
@Size(max = 100, message = "Email deve ter no máximo 100 caracteres")
|
||||
private String email;
|
||||
|
||||
@Size(min = 6, max = 100, message = "Password deve ter entre 6 e 100 caracteres")
|
||||
private String password; // Opcional para updates
|
||||
|
||||
private Boolean isActive;
|
||||
|
||||
private Instant createdAt;
|
||||
|
||||
private Instant updatedAt;
|
||||
|
||||
private List<UUID> roleIds; // IDs dos perfis associados
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package br.gov.sigefp.admin.application.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de perfil/role.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RoleDTO {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "Código é obrigatório")
|
||||
@Size(min = 3, max = 50, message = "Código deve ter entre 3 e 50 caracteres")
|
||||
private String code;
|
||||
|
||||
@NotBlank(message = "Nome é obrigatório")
|
||||
@Size(max = 100, message = "Nome deve ter no máximo 100 caracteres")
|
||||
private String name;
|
||||
|
||||
@Size(max = 500, message = "Descrição deve ter no máximo 500 caracteres")
|
||||
private String description;
|
||||
|
||||
private Set<UUID> permissionIds;
|
||||
}
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package br.gov.sigefp.admin.application.dto;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de utilizador.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserDTO {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "Username é obrigatório")
|
||||
@Size(min = 3, max = 50, message = "Username deve ter entre 3 e 50 caracteres")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "Email é obrigatório")
|
||||
@Email(message = "Email inválido")
|
||||
@Size(max = 100, message = "Email deve ter no máximo 100 caracteres")
|
||||
private String email;
|
||||
|
||||
@Size(min = 6, max = 100, message = "Password deve ter entre 6 e 100 caracteres")
|
||||
private String password; // Opcional para updates
|
||||
|
||||
@NotBlank(message = "Nome completo é obrigatório")
|
||||
@Size(max = 100, message = "Nome completo deve ter no máximo 100 caracteres")
|
||||
private String fullName;
|
||||
|
||||
@Size(max = 20, message = "Telefone deve ter no máximo 20 caracteres")
|
||||
private String phone;
|
||||
|
||||
private Boolean active;
|
||||
|
||||
private LocalDate createdAtDate;
|
||||
|
||||
private LocalDate lastLoginDate;
|
||||
|
||||
private Set<UUID> roleIds;
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package br.gov.sigefp.admin.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
/**
|
||||
* Configuração do módulo admin.
|
||||
*/
|
||||
@Configuration
|
||||
public class AdminConfig {
|
||||
|
||||
@Bean
|
||||
@org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean(PasswordEncoder.class)
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package br.gov.sigefp.admin.domain;
|
||||
|
||||
import br.gov.sigefp.common.domain.AuditableEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* Entidade que representa uma permissão do sistema.
|
||||
* Formato: módulo:recurso:ação (ex: "admin:user:create", "rh:agent:read")
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "permissions", indexes = {
|
||||
@Index(name = "idx_permission_code", columnList = "code")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class Permission extends AuditableEntity {
|
||||
|
||||
@Column(nullable = false, unique = true, length = 100)
|
||||
private String code;
|
||||
|
||||
@Column(nullable = false, length = 200)
|
||||
private String description;
|
||||
|
||||
@Column(length = 50)
|
||||
private String module;
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package br.gov.sigefp.admin.domain;
|
||||
|
||||
import br.gov.sigefp.common.domain.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* Entidade que representa um perfil/role do sistema.
|
||||
* Não possui bidirecionalidade com User (conforme especificado).
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "role", indexes = {
|
||||
@Index(name = "idx_role_code", columnList = "code")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class Role extends BaseEntity {
|
||||
|
||||
@Column(nullable = false, unique = true, length = 50)
|
||||
private String code;
|
||||
|
||||
@Column(nullable = false, length = 200)
|
||||
private String name;
|
||||
|
||||
@Column(length = 500)
|
||||
private String description;
|
||||
}
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package br.gov.sigefp.admin.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entidade que representa uma conta de utilizador do sistema.
|
||||
* Usa Instant para timestamps (createdAt/updatedAt).
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "user_account", indexes = {
|
||||
@Index(name = "idx_user_email", columnList = "email"),
|
||||
@Index(name = "idx_user_username", columnList = "username")
|
||||
})
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class UserAccount {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column(updatable = false, nullable = false)
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 50)
|
||||
private String username;
|
||||
|
||||
@Column(nullable = false, length = 200, name = "full_name")
|
||||
private String fullName;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 100)
|
||||
private String email;
|
||||
|
||||
@Column(nullable = false, name = "password_hash")
|
||||
private String passwordHash; // Hash da senha (BCrypt, etc.)
|
||||
|
||||
@Column(nullable = false, name = "is_active")
|
||||
@Builder.Default
|
||||
private Boolean isActive = true;
|
||||
|
||||
@CreatedDate
|
||||
@Column(nullable = false, updatable = false, name = "created_at")
|
||||
private Instant createdAt;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(nullable = false, name = "updated_at")
|
||||
private Instant updatedAt;
|
||||
|
||||
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@Builder.Default
|
||||
private List<UserRole> userRoles = new ArrayList<>();
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package br.gov.sigefp.admin.domain;
|
||||
|
||||
import br.gov.sigefp.common.domain.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* Entidade de junção explícita para relacionamento N:N entre UserAccount e Role.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "user_role", indexes = {
|
||||
@Index(name = "idx_user_role_user", columnList = "user_id"),
|
||||
@Index(name = "idx_user_role_role", columnList = "role_id"),
|
||||
@Index(name = "idx_user_role_unique", columnList = "user_id,role_id", unique = true)
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class UserRole extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
private UserAccount user;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "role_id", nullable = false)
|
||||
private Role role;
|
||||
}
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package br.gov.sigefp.admin.infrastructure.repository;
|
||||
|
||||
import br.gov.sigefp.admin.domain.Permission;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface PermissionRepository extends JpaRepository<Permission, UUID> {
|
||||
|
||||
Optional<Permission> findByCode(String code);
|
||||
|
||||
boolean existsByCode(String code);
|
||||
}
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package br.gov.sigefp.admin.repository;
|
||||
|
||||
import br.gov.sigefp.admin.domain.Role;
|
||||
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<Role, UUID> {
|
||||
|
||||
Optional<Role> findByCode(String code);
|
||||
|
||||
boolean existsByCode(String code);
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package br.gov.sigefp.admin.repository;
|
||||
|
||||
import br.gov.sigefp.admin.domain.UserAccount;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface UserAccountRepository extends JpaRepository<UserAccount, UUID> {
|
||||
|
||||
/**
|
||||
* Busca todos os utilizadores com seus perfis carregados eagerly.
|
||||
* Usa JOIN FETCH para evitar lazy loading exceptions.
|
||||
*/
|
||||
@Query("SELECT DISTINCT u FROM UserAccount u LEFT JOIN FETCH u.userRoles ur LEFT JOIN FETCH ur.role")
|
||||
Page<UserAccount> findAllWithRoles(Pageable pageable);
|
||||
|
||||
Optional<UserAccount> findByUsername(String username);
|
||||
|
||||
Optional<UserAccount> findByEmail(String email);
|
||||
|
||||
boolean existsByUsername(String username);
|
||||
|
||||
boolean existsByEmail(String email);
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package br.gov.sigefp.admin.repository;
|
||||
|
||||
import br.gov.sigefp.admin.domain.UserRole;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
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.UUID;
|
||||
|
||||
@Repository
|
||||
public interface UserRoleRepository extends JpaRepository<UserRole, UUID> {
|
||||
|
||||
@Query("SELECT ur FROM UserRole ur JOIN FETCH ur.role WHERE ur.user.id = :userId")
|
||||
List<UserRole> findByUserId(@Param("userId") UUID userId);
|
||||
|
||||
@Modifying
|
||||
@Query("DELETE FROM UserRole ur WHERE ur.user.id = :userId")
|
||||
void deleteByUserId(@Param("userId") UUID userId);
|
||||
|
||||
@Query("SELECT CASE WHEN COUNT(ur) > 0 THEN true ELSE false END FROM UserRole ur WHERE ur.user.id = :userId AND ur.role.id = :roleId")
|
||||
boolean existsByUserIdAndRoleId(@Param("userId") UUID userId, @Param("roleId") UUID roleId);
|
||||
}
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package br.gov.sigefp.admin.service;
|
||||
|
||||
import br.gov.sigefp.admin.api.dto.AuditLogDTO;
|
||||
import br.gov.sigefp.admin.domain.UserAccount;
|
||||
import br.gov.sigefp.common.domain.AuditLog;
|
||||
import br.gov.sigefp.common.repository.AuditLogRepository;
|
||||
import br.gov.sigefp.admin.repository.UserAccountRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Serviço de aplicação para gestão de logs de auditoria.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class AuditLogService {
|
||||
|
||||
private final AuditLogRepository auditLogRepository;
|
||||
private final UserAccountRepository userAccountRepository;
|
||||
|
||||
/**
|
||||
* Registra um log de auditoria.
|
||||
* Pode ser usado por outros módulos.
|
||||
*/
|
||||
public AuditLogDTO logAction(UUID userId, String module, String action, String entity, UUID entityId,
|
||||
String description) {
|
||||
String username = "system";
|
||||
if (userId != null) {
|
||||
username = userAccountRepository.findById(userId)
|
||||
.map(UserAccount::getUsername)
|
||||
.orElse("unknown");
|
||||
}
|
||||
|
||||
AuditLog auditLog = AuditLog.builder()
|
||||
.userId(userId)
|
||||
.username(username)
|
||||
.module(module)
|
||||
.action(action)
|
||||
.entity(entity)
|
||||
.entityId(entityId)
|
||||
.description(description)
|
||||
.build();
|
||||
|
||||
AuditLog saved = auditLogRepository.save(auditLog);
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
/**
|
||||
* Consulta logs com filtros opcionais.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<AuditLogDTO> findLogs(UUID userId, String module, Instant startDate, Instant endDate,
|
||||
Pageable pageable) {
|
||||
Page<AuditLog> logs;
|
||||
|
||||
if (userId != null && module != null && startDate != null && endDate != null) {
|
||||
logs = auditLogRepository.findByUserIdAndModuleAndCreatedAtBetween(userId, module, startDate, endDate,
|
||||
pageable);
|
||||
} else if (userId != null && module != null) {
|
||||
logs = auditLogRepository.findByUserIdAndModule(userId, module, pageable);
|
||||
} else if (userId != null) {
|
||||
logs = auditLogRepository.findByUserId(userId, pageable);
|
||||
} else if (module != null) {
|
||||
logs = auditLogRepository.findByModule(module, pageable);
|
||||
} else if (startDate != null && endDate != null) {
|
||||
logs = auditLogRepository.findByCreatedAtBetween(startDate, endDate, pageable);
|
||||
} else {
|
||||
logs = auditLogRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
return logs.map(this::toDTO);
|
||||
}
|
||||
|
||||
private AuditLogDTO toDTO(AuditLog log) {
|
||||
return AuditLogDTO.builder()
|
||||
.id(log.getId())
|
||||
.userId(log.getUserId())
|
||||
.userUsername(log.getUsername())
|
||||
.module(log.getModule())
|
||||
.action(log.getAction())
|
||||
.entity(log.getEntity())
|
||||
.entityId(log.getEntityId())
|
||||
.description(log.getDescription())
|
||||
.createdAt(log.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package br.gov.sigefp.admin.service;
|
||||
|
||||
import br.gov.sigefp.admin.api.dto.RoleDTO;
|
||||
import br.gov.sigefp.admin.domain.Role;
|
||||
import br.gov.sigefp.admin.repository.RoleRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Serviço de aplicação para gestão de perfis/roles.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class RoleService {
|
||||
|
||||
private final RoleRepository roleRepository;
|
||||
|
||||
public RoleDTO create(RoleDTO dto) {
|
||||
if (roleRepository.existsByCode(dto.getCode())) {
|
||||
throw new IllegalArgumentException("Código de perfil já existe: " + dto.getCode());
|
||||
}
|
||||
|
||||
Role role = Role.builder()
|
||||
.code(dto.getCode())
|
||||
.name(dto.getName())
|
||||
.description(dto.getDescription())
|
||||
.build();
|
||||
|
||||
Role saved = roleRepository.save(role);
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
public RoleDTO update(UUID id, RoleDTO dto) {
|
||||
Role role = roleRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Perfil não encontrado: " + id));
|
||||
|
||||
if (dto.getCode() != null && !dto.getCode().equals(role.getCode())) {
|
||||
if (roleRepository.existsByCode(dto.getCode())) {
|
||||
throw new IllegalArgumentException("Código de perfil já existe: " + dto.getCode());
|
||||
}
|
||||
role.setCode(dto.getCode());
|
||||
}
|
||||
|
||||
if (dto.getName() != null) {
|
||||
role.setName(dto.getName());
|
||||
}
|
||||
if (dto.getDescription() != null) {
|
||||
role.setDescription(dto.getDescription());
|
||||
}
|
||||
|
||||
Role saved = roleRepository.save(role);
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public RoleDTO findById(UUID id) {
|
||||
Role role = roleRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Perfil não encontrado: " + id));
|
||||
return toDTO(role);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<RoleDTO> findAll(Pageable pageable) {
|
||||
return roleRepository.findAll(pageable).map(this::toDTO);
|
||||
}
|
||||
|
||||
private RoleDTO toDTO(Role role) {
|
||||
return RoleDTO.builder()
|
||||
.id(role.getId())
|
||||
.code(role.getCode())
|
||||
.name(role.getName())
|
||||
.description(role.getDescription())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package br.gov.sigefp.admin.service;
|
||||
|
||||
import br.gov.sigefp.admin.api.dto.UserAccountDTO;
|
||||
import br.gov.sigefp.admin.domain.Role;
|
||||
import br.gov.sigefp.admin.domain.UserAccount;
|
||||
import br.gov.sigefp.admin.domain.UserRole;
|
||||
import br.gov.sigefp.admin.repository.RoleRepository;
|
||||
import br.gov.sigefp.admin.repository.UserAccountRepository;
|
||||
import br.gov.sigefp.admin.repository.UserRoleRepository;
|
||||
import br.gov.sigefp.common.service.GlobalAuditLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Serviço de aplicação para gestão de utilizadores.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class UserService {
|
||||
|
||||
private final UserAccountRepository userAccountRepository;
|
||||
private final RoleRepository roleRepository;
|
||||
private final UserRoleRepository userRoleRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final GlobalAuditLogService auditLogService;
|
||||
|
||||
public UserAccountDTO create(UserAccountDTO dto) {
|
||||
if (userAccountRepository.existsByUsername(dto.getUsername())) {
|
||||
throw new IllegalArgumentException("Username já existe: " + dto.getUsername());
|
||||
}
|
||||
if (userAccountRepository.existsByEmail(dto.getEmail())) {
|
||||
throw new IllegalArgumentException("Email já existe: " + dto.getEmail());
|
||||
}
|
||||
|
||||
UserAccount user = UserAccount.builder()
|
||||
.username(dto.getUsername())
|
||||
.fullName(dto.getFullName())
|
||||
.email(dto.getEmail())
|
||||
.passwordHash(passwordEncoder.encode(dto.getPassword()))
|
||||
.isActive(dto.getIsActive() != null ? dto.getIsActive() : true)
|
||||
.build();
|
||||
|
||||
UserAccount saved = userAccountRepository.save(user);
|
||||
|
||||
// Associar perfis se fornecidos
|
||||
if (dto.getRoleIds() != null && !dto.getRoleIds().isEmpty()) {
|
||||
assignRoles(saved.getId(), dto.getRoleIds());
|
||||
}
|
||||
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
public UserAccountDTO update(UUID id, UserAccountDTO dto) {
|
||||
UserAccount user = userAccountRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Utilizador não encontrado: " + id));
|
||||
|
||||
StringBuilder log = new StringBuilder();
|
||||
|
||||
if (dto.getEmail() != null && !dto.getEmail().equals(user.getEmail())) {
|
||||
if (userAccountRepository.existsByEmail(dto.getEmail())) {
|
||||
throw new IllegalArgumentException("Email já existe: " + dto.getEmail());
|
||||
}
|
||||
log.append("Email: [").append(user.getEmail()).append("] -> [").append(dto.getEmail()).append("]. ");
|
||||
user.setEmail(dto.getEmail());
|
||||
}
|
||||
|
||||
if (dto.getFullName() != null && !dto.getFullName().equals(user.getFullName())) {
|
||||
log.append("Nome: [").append(user.getFullName()).append("] -> [").append(dto.getFullName()).append("]. ");
|
||||
user.setFullName(dto.getFullName());
|
||||
}
|
||||
if (dto.getIsActive() != null && !dto.getIsActive().equals(user.getIsActive())) {
|
||||
log.append("Ativo: [").append(user.getIsActive()).append("] -> [").append(dto.getIsActive()).append("]. ");
|
||||
user.setIsActive(dto.getIsActive());
|
||||
}
|
||||
if (dto.getPassword() != null && !dto.getPassword().isEmpty()) {
|
||||
log.append("Senha alterada. ");
|
||||
user.setPasswordHash(passwordEncoder.encode(dto.getPassword()));
|
||||
}
|
||||
|
||||
UserAccount saved = userAccountRepository.save(user);
|
||||
|
||||
if (log.length() > 0) {
|
||||
// Nota: Em um cenário real, o userId viria do SecurityContextHolder
|
||||
auditLogService.logAction(id, saved.getUsername(), "ADMIN", "UPDATE", "UserAccount", id, log.toString());
|
||||
}
|
||||
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public UserAccountDTO findById(UUID id) {
|
||||
UserAccount user = userAccountRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Utilizador não encontrado: " + id));
|
||||
return toDTO(user);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<UserAccountDTO> findAll(Pageable pageable) {
|
||||
Page<UserAccount> users = userAccountRepository.findAllWithRoles(pageable);
|
||||
return users.map(this::toDTO);
|
||||
}
|
||||
|
||||
public void assignRoles(UUID userId, List<UUID> roleIds) {
|
||||
UserAccount user = userAccountRepository.findById(userId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Utilizador não encontrado: " + userId));
|
||||
|
||||
// Remover perfis existentes
|
||||
userRoleRepository.deleteByUserId(userId);
|
||||
|
||||
// Adicionar novos perfis
|
||||
for (UUID roleId : roleIds) {
|
||||
Role role = roleRepository.findById(roleId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Perfil não encontrado: " + roleId));
|
||||
|
||||
if (!userRoleRepository.existsByUserIdAndRoleId(userId, roleId)) {
|
||||
UserRole userRole = UserRole.builder()
|
||||
.user(user)
|
||||
.role(role)
|
||||
.build();
|
||||
userRoleRepository.save(userRole);
|
||||
}
|
||||
}
|
||||
|
||||
auditLogService.logAction(userId, user.getUsername(), "ADMIN", "ASSIGN_ROLES", "UserAccount", userId,
|
||||
"Perfis atualizados: " + roleIds.toString());
|
||||
}
|
||||
|
||||
public void removeRoles(UUID userId, List<UUID> roleIds) {
|
||||
UserAccount user = userAccountRepository.findById(userId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Utilizador não encontrado: " + userId));
|
||||
|
||||
for (UUID roleId : roleIds) {
|
||||
List<UserRole> userRoles = userRoleRepository.findByUserId(userId);
|
||||
userRoles.stream()
|
||||
.filter(ur -> ur.getRole().getId().equals(roleId))
|
||||
.forEach(userRoleRepository::delete);
|
||||
}
|
||||
}
|
||||
|
||||
private UserAccountDTO toDTO(UserAccount user) {
|
||||
List<UUID> roleIds = List.of();
|
||||
try {
|
||||
// Tentar usar o relacionamento carregado primeiro
|
||||
if (user.getUserRoles() != null && !user.getUserRoles().isEmpty()) {
|
||||
roleIds = user.getUserRoles().stream()
|
||||
.map(ur -> {
|
||||
try {
|
||||
return ur.getRole().getId();
|
||||
} catch (Exception e) {
|
||||
// Se houver erro de lazy loading, buscar via repository
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(id -> id != null)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// Se não conseguiu via relacionamento, buscar via repository
|
||||
if (roleIds.isEmpty()) {
|
||||
roleIds = userRoleRepository.findByUserId(user.getId()).stream()
|
||||
.map(ur -> ur.getRole().getId())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("Erro ao buscar roles para usuário " + user.getId() + ": " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
roleIds = List.of();
|
||||
}
|
||||
|
||||
return UserAccountDTO.builder()
|
||||
.id(user.getId())
|
||||
.username(user.getUsername())
|
||||
.fullName(user.getFullName())
|
||||
.email(user.getEmail())
|
||||
.isActive(user.getIsActive())
|
||||
.createdAt(user.getCreatedAt())
|
||||
.updatedAt(user.getUpdatedAt())
|
||||
.roleIds(roleIds)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user