feat: otimização de performance e ajustes finais

This commit is contained in:
Idrissa Banora
2026-05-18 10:49:32 +00:00
commit 52a7c4f9cf
579 changed files with 156489 additions and 0 deletions
@@ -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>
@@ -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);
}
}
@@ -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();
}
}
}
@@ -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();
}
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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
}
@@ -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;
}
@@ -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;
}
@@ -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();
}
}
@@ -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;
}
@@ -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;
}
@@ -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<>();
}
@@ -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;
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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();
}
}