diff --git a/src/main/java/com/security/clients/MembersClient.java b/src/main/java/com/security/clients/MembersClient.java deleted file mode 100644 index 812eb21..0000000 --- a/src/main/java/com/security/clients/MembersClient.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.security.clients; - - -public interface MembersClient { -} diff --git a/src/main/java/com/security/controllers/AdminUserController.java b/src/main/java/com/security/controllers/AdminUserController.java index 70b4e94..2d56af8 100644 --- a/src/main/java/com/security/controllers/AdminUserController.java +++ b/src/main/java/com/security/controllers/AdminUserController.java @@ -4,6 +4,8 @@ import com.security.dtos.autorization.RoleChangeRequestDTO; import com.security.dtos.autorization.RolesResponseDTO; import com.security.annotations.AdminAccess; +import com.security.dtos.roles.RoleDetailsDto; +import com.security.dtos.roles.RoleStatisticsDto; import com.security.services.UserAccountService; import com.security.services.UserRoleService; import io.swagger.v3.oas.annotations.Operation; @@ -14,6 +16,8 @@ import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.Map; import java.util.UUID; @RestController @@ -32,22 +36,22 @@ public ResponseEntity getRoles(@PathVariable UUID id) { return ResponseEntity.ok(userRoleService.getUserRoles(id)); } - @Operation(summary = "Agregar un rol a un usuario") @PostMapping("/{id}/roles") @AdminAccess - public ResponseEntity addRole(@PathVariable UUID id, @Valid @RequestBody RoleChangeRequestDTO request) { + public ResponseEntity addRole(@PathVariable UUID id, + @Valid @RequestBody RoleChangeRequestDTO request) { return ResponseEntity.ok(userRoleService.addRoleToUser(id, request.role())); } @Operation(summary = "Quitar un rol a un usuario") @DeleteMapping("/{id}/roles") @AdminAccess - public ResponseEntity deleteRole(@PathVariable UUID id, @RequestBody RoleChangeRequestDTO requestDTO) { + public ResponseEntity deleteRole(@PathVariable UUID id, + @RequestBody RoleChangeRequestDTO requestDTO) { return ResponseEntity.ok(userRoleService.removeRoleFromUser(id, requestDTO.role())); } - @Operation(summary = "Desactivar cuenta de usuario") @PatchMapping("/{id}/deactivate") @AdminAccess @@ -69,4 +73,25 @@ public ResponseEntity activate(@PathVariable UUID id, return ResponseEntity.ok(userAccountService.activateUser(id, admin)); } + @Operation(summary = "Obtener estadísticas de usuarios por roles y estados") + @GetMapping("") + @AdminAccess + public ResponseEntity getUserStatistics() { + return ResponseEntity.ok(userRoleService.getUserStatistics()); + } + + @Operation(summary = "Obtener lista de roles con descripciones") + @GetMapping("/roles") + @AdminAccess + public ResponseEntity> getRoleDetails() { + return ResponseEntity.ok(userRoleService.getRoleDetails()); + } + + @Operation(summary = "Obtener estadísticas de usuarios") + @GetMapping("/provider") + @AdminAccess + public ResponseEntity> getUserProviderStatistics() { + return ResponseEntity.ok(userAccountService.getUserStatistics()); + } + } diff --git a/src/main/java/com/security/dtos/autorization/UserDTO.java b/src/main/java/com/security/dtos/autorization/UserDTO.java index 60f6fea..6f50db7 100644 --- a/src/main/java/com/security/dtos/autorization/UserDTO.java +++ b/src/main/java/com/security/dtos/autorization/UserDTO.java @@ -10,18 +10,12 @@ @NoArgsConstructor @AllArgsConstructor public class UserDTO { - private UUID id; - private String username; - private String email; - private String firstName; - private String lastName; - private Boolean enabled; - + private String provider; private Set roles; } \ No newline at end of file diff --git a/src/main/java/com/security/dtos/roles/RoleDetailsDto.java b/src/main/java/com/security/dtos/roles/RoleDetailsDto.java new file mode 100644 index 0000000..d0aaa37 --- /dev/null +++ b/src/main/java/com/security/dtos/roles/RoleDetailsDto.java @@ -0,0 +1,9 @@ +package com.security.dtos.roles; + +import lombok.*; +@Data +@AllArgsConstructor +public class RoleDetailsDto { + private String roleName; + private String description; +} diff --git a/src/main/java/com/security/dtos/roles/RoleStatisticsDto.java b/src/main/java/com/security/dtos/roles/RoleStatisticsDto.java new file mode 100644 index 0000000..94c009b --- /dev/null +++ b/src/main/java/com/security/dtos/roles/RoleStatisticsDto.java @@ -0,0 +1,16 @@ +package com.security.dtos.roles; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.Map; + +@Data +@AllArgsConstructor +public class RoleStatisticsDto { + private long totalUsers; + private long activeUsers; + private long inactiveUsers; + private long suspendedUsers; + private Map roleCounts; +} diff --git a/src/main/java/com/security/entity/RoleEntity.java b/src/main/java/com/security/entity/RoleEntity.java index c45d4d8..5742033 100644 --- a/src/main/java/com/security/entity/RoleEntity.java +++ b/src/main/java/com/security/entity/RoleEntity.java @@ -1,6 +1,7 @@ package com.security.entity; import com.security.config.audit.Audit; +import com.security.config.audit.AuditListener; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; @@ -15,6 +16,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@EntityListeners(AuditListener.class) public class RoleEntity { @Id @GeneratedValue(strategy = GenerationType.UUID) diff --git a/src/main/java/com/security/mappers/UserMapper.java b/src/main/java/com/security/mappers/UserMapper.java index 84634ef..0c08dfd 100644 --- a/src/main/java/com/security/mappers/UserMapper.java +++ b/src/main/java/com/security/mappers/UserMapper.java @@ -11,9 +11,9 @@ public interface UserMapper { @Mapping(target = "roles", source = "roles") + @Mapping(target = "provider", expression = "java(userEntity.getProvider() != null ? userEntity.getProvider().name() : null)") UserDTO toDTO(UserEntity userEntity); -// @Mapping(target = "roles", source = "roles") SimpleUserDto toSimpleDto(UserEntity user); @Mapping(target = "password", ignore = true) diff --git a/src/main/java/com/security/repository/RoleRepository.java b/src/main/java/com/security/repository/RoleRepository.java index 9b0e5c4..cb4eda8 100644 --- a/src/main/java/com/security/repository/RoleRepository.java +++ b/src/main/java/com/security/repository/RoleRepository.java @@ -2,10 +2,12 @@ import com.security.entity.RoleEntity; 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 { Optional findByName(String name); } diff --git a/src/main/java/com/security/repository/UserRepository.java b/src/main/java/com/security/repository/UserRepository.java index 9a91359..3b37ab8 100644 --- a/src/main/java/com/security/repository/UserRepository.java +++ b/src/main/java/com/security/repository/UserRepository.java @@ -2,14 +2,13 @@ import com.security.entity.UserEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import java.util.Optional; import java.util.UUID; +@Repository public interface UserRepository extends JpaRepository { Optional findByEmail(String email); - boolean existsByEmail(String email); - - } diff --git a/src/main/java/com/security/services/Impl/AuthServiceImpl.java b/src/main/java/com/security/services/Impl/AuthServiceImpl.java index 790aa9a..af5db42 100644 --- a/src/main/java/com/security/services/Impl/AuthServiceImpl.java +++ b/src/main/java/com/security/services/Impl/AuthServiceImpl.java @@ -15,6 +15,9 @@ import com.security.repository.UserRepository; import com.security.services.AuthService; import com.security.services.TokenService; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.retry.annotation.Retry; +import io.github.resilience4j.timelimiter.annotation.TimeLimiter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.kafka.core.KafkaTemplate; @@ -40,7 +43,6 @@ public class AuthServiceImpl implements AuthService { private final PasswordEncoder passwordEncoder; private final RoleRepository roleRepository; private final UserMapper userMapper; - private final JwtEncoder jwtEncoder; private final JwtDecoder jwtDecoder; private final KafkaTemplate kafkaTemplate; private final TokenService tokenService; @@ -50,6 +52,8 @@ public class AuthServiceImpl implements AuthService { private final Set blacklistedTokens = ConcurrentHashMap.newKeySet(); @Override + @CircuitBreaker(name = "authServiceCircuitBreaker", fallbackMethod = "authenticateUserFallback") + @Retry(name = "databaseRetry") public LoginResponseDTO authenticateUser(LoginRequestDTO request) { log.info("Authenticating user: {}", request.getEmail()); @@ -80,7 +84,14 @@ public LoginResponseDTO authenticateUser(LoginRequestDTO request) { .build(); } + private LoginResponseDTO authenticateUserFallback(LoginResponseDTO request, Throwable throwable) { + log.error("Fallback activado para authenticateUser - Error {}", throwable.getMessage()); + throw new AuthenticationException("Servicio de autenticación no disponible.Intente de nuevo más tarde."); + } + @Override + @CircuitBreaker(name = "authServiceCircuitBreaker", fallbackMethod = "refreshTokenFallback") + @Retry(name = "databaseRetry") public LoginResponseDTO refreshToken(String refreshToken) { log.info("Refreshing token"); @@ -123,6 +134,11 @@ public LoginResponseDTO refreshToken(String refreshToken) { } } + private LoginResponseDTO refreshTokenFallback(String refreshToken, Throwable throwable) { + log.error("Fallback activado para refreshToken - Error: {}", throwable.getMessage()); + throw new AuthenticationException("Servicio de renovación de tokens no disponible"); + } + @Override public LoginResponseDTO createTokensForOAuth2User(UserEntity user) { log.info("Creando tokens para usuarios OAuth2: {}", user.getEmail()); @@ -171,6 +187,8 @@ public void logout(String accessToken) { @Override + @CircuitBreaker(name = "authServiceCircuitBreaker", fallbackMethod = "registerUserFallback") + @Retry(name = "databaseRetry") public LoginResponseDTO registerUser(RegisterRequestDto registerRequestDto) { if (userRepository.existsByEmail(registerRequestDto.email())) { throw new AuthenticationException("Error al registrar usuario intente de nuevo"); @@ -190,19 +208,7 @@ public LoginResponseDTO registerUser(RegisterRequestDto registerRequestDto) { userRepository.save(user); - CreatedUserEvent event = new CreatedUserEvent( - user.getId().toString(), - registerRequestDto.firstName(), - registerRequestDto.lastName(), - registerRequestDto.dni(), - registerRequestDto.phone(), - registerRequestDto.email(), - null - ); - - log.info("Enviando evento {}", event); - kafkaTemplate.send("user-created-event-topic", event); - log.info("Evento enviado {}", event); + sendUserCreatedEvent(user, registerRequestDto); return LoginResponseDTO.builder() .accessToken(tokenService.generateAccessToken(user)) @@ -215,5 +221,32 @@ public LoginResponseDTO registerUser(RegisterRequestDto registerRequestDto) { .build(); } + private LoginResponseDTO registerUserFallback(RegisterRequestDto registerRequestDto, Throwable throwable) { + log.error("Fallback activado para registerUser- Error {}", throwable.getMessage()); + throw new AuthenticationException("Servicio de registro temporalmente no disponible"); + } + + @CircuitBreaker(name = "kafkaCircuitBreaker", fallbackMethod = "sendUserCreatedEventFallback") + @Retry(name = "kafkaRetry") + private void sendUserCreatedEvent(UserEntity user, RegisterRequestDto registerRequestDto) { + CreatedUserEvent event = new CreatedUserEvent( + user.getId().toString(), + registerRequestDto.firstName(), + registerRequestDto.lastName(), + registerRequestDto.dni(), + registerRequestDto.phone(), + registerRequestDto.email(), + null + ); + + log.info("Enviando evento de usuario creado: {}", event); + kafkaTemplate.send("user-created-event-topic", event); + log.info("Evento enviado correctamente"); + } + + private void sendUserCreatedEventFallback(UserEntity user, RegisterRequestDto registerRequestDto, Throwable throwable) { + log.error("Fallback de Kafka - No se pudo enviar evento de usuario creado. Error: {}", throwable.getMessage()); + log.warn("Evento no enviado será reintentado posteriormente para usuario: {}", user.getEmail()); + } } \ No newline at end of file diff --git a/src/main/java/com/security/services/Impl/UserAccountServiceImpl.java b/src/main/java/com/security/services/Impl/UserAccountServiceImpl.java index 4417ec6..6124a89 100644 --- a/src/main/java/com/security/services/Impl/UserAccountServiceImpl.java +++ b/src/main/java/com/security/services/Impl/UserAccountServiceImpl.java @@ -2,6 +2,7 @@ import com.security.dtos.auth.AuthResponseDTO; import com.security.entity.UserEntity; +import com.security.enums.AuthProvider; import com.security.repository.UserRepository; import com.security.config.audit.Audit; import com.security.services.UserAccountService; @@ -12,6 +13,7 @@ import org.springframework.web.server.ResponseStatusException; import java.time.Instant; +import java.util.Map; import java.util.UUID; @Service @@ -47,6 +49,24 @@ public AuthResponseDTO deactivateUser(UUID userId, String reason, String admin) } + @Override + @Transactional(readOnly = true) + public Map getUserStatistics() { + long totalUsers = userRepository.count(); + long googleUsers = userRepository.findAll().stream() + .filter(user -> user.getProvider() == AuthProvider.GOOGLE) + .count(); + long localUsers = userRepository.findAll().stream() + .filter(user -> user.getProvider() == AuthProvider.LOCAL) + .count(); + + return Map.of( + "totalUsers", totalUsers, + "googleUsers", googleUsers, + "localUsers", localUsers + ); + } + @Override public AuthResponseDTO activateUser(UUID userId, String admin) { diff --git a/src/main/java/com/security/services/Impl/UserRoleServiceImpl.java b/src/main/java/com/security/services/Impl/UserRoleServiceImpl.java index 37f814c..f73057b 100644 --- a/src/main/java/com/security/services/Impl/UserRoleServiceImpl.java +++ b/src/main/java/com/security/services/Impl/UserRoleServiceImpl.java @@ -2,6 +2,8 @@ import com.security.dtos.auth.AuthResponseDTO; import com.security.dtos.autorization.RolesResponseDTO; +import com.security.dtos.roles.RoleDetailsDto; +import com.security.dtos.roles.RoleStatisticsDto; import com.security.entity.RoleEntity; import com.security.entity.UserEntity; import com.security.repository.RoleRepository; @@ -15,9 +17,7 @@ import org.springframework.web.server.ResponseStatusException; import java.time.Instant; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; @Service @@ -70,4 +70,27 @@ public AuthResponseDTO removeRoleFromUser(UUID userId, String roleName) { userRepository.save(user); return new AuthResponseDTO(true, "Rol removido", Instant.now()); } + + @Override + public RoleStatisticsDto getUserStatistics() { + List users = userRepository.findAll(); + + long totalUsers = users.size(); + long activeUsers = users.stream().filter(UserEntity::isEnabled).count(); + long inactiveUsers = users.stream().filter(user -> !user.isEnabled()).count(); + long suspendedUsers = users.stream().filter(user -> !user.isAccountNonLocked()).count(); + + Map roleCounts = users.stream() + .flatMap(user -> user.getRoles().stream()) + .collect(Collectors.groupingBy(role -> role.getName().toUpperCase(), Collectors.counting())); + + return new RoleStatisticsDto(totalUsers, activeUsers, inactiveUsers, suspendedUsers, roleCounts); + } + + @Override + public List getRoleDetails() { + return roleRepository.findAll().stream() + .map(role -> new RoleDetailsDto(role.getName(), role.getDescription())) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/com/security/services/Impl/UserServiceImpl.java b/src/main/java/com/security/services/Impl/UserServiceImpl.java index 221888b..00c3142 100644 --- a/src/main/java/com/security/services/Impl/UserServiceImpl.java +++ b/src/main/java/com/security/services/Impl/UserServiceImpl.java @@ -3,9 +3,14 @@ import com.security.dtos.autorization.UserDTO; import com.security.dtos.chat.SimpleUserDto; import com.security.entity.UserEntity; +import com.security.exceptions.RoleNotFoundException; +import com.security.exceptions.UserNotFoundException; import com.security.mappers.UserMapper; import com.security.repository.UserRepository; import com.security.services.UserService; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.retry.annotation.Retry; +import io.github.resilience4j.timelimiter.annotation.TimeLimiter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -14,7 +19,7 @@ import java.util.List; import java.util.UUID; -import java.util.stream.Collectors; + @Slf4j @Service @@ -24,15 +29,23 @@ public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final UserMapper userMapper; + @CircuitBreaker(name = "userServiceCircuitBreaker", fallbackMethod = "getUserByIdFallback") + @Retry(name = "databaseRetry") @Transactional(readOnly = true) @Override public UserDTO getUserById(UUID id) { - UserEntity user = userRepository.findById(id) .orElseThrow(() -> new UsernameNotFoundException("Usuario no encontrado con id: " + id)); return userMapper.toDTO(user); } + public UserDTO getUserByIdFallback(UUID id, Throwable ex) { + log.error("Error al obtener al usuario con id : {} {}", id, ex.getMessage()); + throw new UserNotFoundException("Error al obtener al usuario con id " + id); + } + + @CircuitBreaker(name = "userServiceCircuitBreaker", fallbackMethod = "getUserByEmailFallback") + @Retry(name = "databaseRetry") @Transactional(readOnly = true) @Override public UserDTO getUserByEmail(String email) { @@ -42,6 +55,13 @@ public UserDTO getUserByEmail(String email) { return userMapper.toDTO(user); } + public UserDTO getUserByEmailFallback(String email, Throwable ex) { + log.error("Error al obtener usuario por email {} ejecutando fallback {}", email, ex.getMessage()); + throw new UsernameNotFoundException("Usuario no encontrado con email" + email); + } + + @CircuitBreaker(name = "userServiceCircuitBreaker", fallbackMethod = "getUsersByRoleFallback") + @Retry(name = "databaseRetry") @Transactional(readOnly = true) @Override public List getUsersByRole(String roleName) { @@ -52,10 +72,15 @@ public List getUsersByRole(String roleName) { .anyMatch(role -> role.getName().equalsIgnoreCase(roleName))) .toList(); - log.info("✅ Se encontraron {} usuarios con rol {}", users.size(), roleName); - log.info(users.toString()); - + log.info("Se encontraron {} usuarios con rol {}", users.size(), roleName); return users.stream() .map(userMapper::toSimpleDto).toList(); } + + public List getUsersByRoleFallback(String roleName, Throwable ex) { + log.error("Error al encontrar usuario con el rol {} ejecutanfo fallback {}", roleName, ex.getMessage()); + throw new RoleNotFoundException("Rol no encontrado: " + roleName); + } + + } diff --git a/src/main/java/com/security/services/UserAccountService.java b/src/main/java/com/security/services/UserAccountService.java index 4868ed1..63a3035 100644 --- a/src/main/java/com/security/services/UserAccountService.java +++ b/src/main/java/com/security/services/UserAccountService.java @@ -2,10 +2,11 @@ import com.security.dtos.auth.AuthResponseDTO; +import java.util.Map; import java.util.UUID; public interface UserAccountService { AuthResponseDTO deactivateUser(UUID userId, String reason, String admin); - + Map getUserStatistics(); AuthResponseDTO activateUser(UUID userId, String admin); } diff --git a/src/main/java/com/security/services/UserRoleService.java b/src/main/java/com/security/services/UserRoleService.java index 1df3b26..8599602 100644 --- a/src/main/java/com/security/services/UserRoleService.java +++ b/src/main/java/com/security/services/UserRoleService.java @@ -2,14 +2,18 @@ import com.security.dtos.auth.AuthResponseDTO; import com.security.dtos.autorization.RolesResponseDTO; +import com.security.dtos.roles.RoleDetailsDto; +import com.security.dtos.roles.RoleStatisticsDto; +import java.util.List; +import java.util.Map; import java.util.UUID; public interface UserRoleService { RolesResponseDTO getUserRoles(UUID userId); - AuthResponseDTO addRoleToUser(UUID userId, String roleName); - AuthResponseDTO removeRoleFromUser(UUID userId, String roleName); + RoleStatisticsDto getUserStatistics(); + List getRoleDetails(); }