diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaArticleRepository.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaArticleRepository.java index 7da7dd7..91d780d 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaArticleRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaArticleRepository.java @@ -4,6 +4,8 @@ import de.effigenix.domain.masterdata.*; import de.effigenix.infrastructure.masterdata.persistence.mapper.ArticleMapper; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -17,6 +19,8 @@ import java.util.stream.Collectors; @Transactional(readOnly = true) public class JpaArticleRepository implements ArticleRepository { + private static final Logger logger = LoggerFactory.getLogger(JpaArticleRepository.class); + private final ArticleJpaRepository jpaRepository; private final ArticleMapper mapper; @@ -32,6 +36,7 @@ public class JpaArticleRepository implements ArticleRepository { .map(mapper::toDomain); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findById", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -44,6 +49,7 @@ public class JpaArticleRepository implements ArticleRepository { .collect(Collectors.toList()); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findAll", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -56,6 +62,7 @@ public class JpaArticleRepository implements ArticleRepository { .collect(Collectors.toList()); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findByCategory", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -68,6 +75,7 @@ public class JpaArticleRepository implements ArticleRepository { .collect(Collectors.toList()); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findByStatus", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -79,6 +87,7 @@ public class JpaArticleRepository implements ArticleRepository { jpaRepository.save(mapper.toEntity(article)); return Result.success(null); } catch (Exception e) { + logger.trace("Database error in save", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -90,6 +99,7 @@ public class JpaArticleRepository implements ArticleRepository { jpaRepository.deleteById(article.id().value()); return Result.success(null); } catch (Exception e) { + logger.trace("Database error in delete", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -99,6 +109,7 @@ public class JpaArticleRepository implements ArticleRepository { try { return Result.success(jpaRepository.existsByArticleNumber(articleNumber.value())); } catch (Exception e) { + logger.trace("Database error in existsByArticleNumber", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaCustomerRepository.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaCustomerRepository.java index 311b87a..e64a1fe 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaCustomerRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaCustomerRepository.java @@ -5,6 +5,8 @@ import de.effigenix.infrastructure.masterdata.persistence.entity.FrameContractEn import de.effigenix.infrastructure.masterdata.persistence.mapper.CustomerMapper; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -18,6 +20,8 @@ import java.util.stream.Collectors; @Transactional(readOnly = true) public class JpaCustomerRepository implements CustomerRepository { + private static final Logger logger = LoggerFactory.getLogger(JpaCustomerRepository.class); + private final CustomerJpaRepository jpaRepository; private final FrameContractJpaRepository frameContractJpaRepository; private final CustomerMapper mapper; @@ -40,6 +44,7 @@ public class JpaCustomerRepository implements CustomerRepository { var fcEntity = frameContractJpaRepository.findByCustomerId(id.value()).orElse(null); return Result.success(Optional.of(mapper.toDomain(entityOpt.get(), fcEntity))); } catch (Exception e) { + logger.trace("Database error in findById", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -55,6 +60,7 @@ public class JpaCustomerRepository implements CustomerRepository { .collect(Collectors.toList()); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findAll", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -70,6 +76,7 @@ public class JpaCustomerRepository implements CustomerRepository { .collect(Collectors.toList()); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findByType", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -85,6 +92,7 @@ public class JpaCustomerRepository implements CustomerRepository { .collect(Collectors.toList()); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findByStatus", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -104,6 +112,7 @@ public class JpaCustomerRepository implements CustomerRepository { return Result.success(null); } catch (Exception e) { + logger.trace("Database error in save", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -116,6 +125,7 @@ public class JpaCustomerRepository implements CustomerRepository { jpaRepository.deleteById(customer.id().value()); return Result.success(null); } catch (Exception e) { + logger.trace("Database error in delete", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -125,6 +135,7 @@ public class JpaCustomerRepository implements CustomerRepository { try { return Result.success(jpaRepository.existsByName(name.value())); } catch (Exception e) { + logger.trace("Database error in existsByName", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaProductCategoryRepository.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaProductCategoryRepository.java index 8828fc6..654f772 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaProductCategoryRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaProductCategoryRepository.java @@ -7,6 +7,8 @@ import de.effigenix.domain.masterdata.ProductCategoryRepository; import de.effigenix.infrastructure.masterdata.persistence.mapper.ProductCategoryMapper; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -20,6 +22,8 @@ import java.util.stream.Collectors; @Transactional(readOnly = true) public class JpaProductCategoryRepository implements ProductCategoryRepository { + private static final Logger logger = LoggerFactory.getLogger(JpaProductCategoryRepository.class); + private final ProductCategoryJpaRepository jpaRepository; private final ProductCategoryMapper mapper; @@ -35,6 +39,7 @@ public class JpaProductCategoryRepository implements ProductCategoryRepository { .map(mapper::toDomain); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findById", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -47,6 +52,7 @@ public class JpaProductCategoryRepository implements ProductCategoryRepository { .collect(Collectors.toList()); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findAll", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -58,6 +64,7 @@ public class JpaProductCategoryRepository implements ProductCategoryRepository { jpaRepository.save(mapper.toEntity(category)); return Result.success(null); } catch (Exception e) { + logger.trace("Database error in save", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -69,6 +76,7 @@ public class JpaProductCategoryRepository implements ProductCategoryRepository { jpaRepository.deleteById(category.id().value()); return Result.success(null); } catch (Exception e) { + logger.trace("Database error in delete", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -78,6 +86,7 @@ public class JpaProductCategoryRepository implements ProductCategoryRepository { try { return Result.success(jpaRepository.existsByName(name.value())); } catch (Exception e) { + logger.trace("Database error in existsByName", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaSupplierRepository.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaSupplierRepository.java index 92efd78..072eb83 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaSupplierRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaSupplierRepository.java @@ -4,6 +4,8 @@ import de.effigenix.domain.masterdata.*; import de.effigenix.infrastructure.masterdata.persistence.mapper.SupplierMapper; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -17,6 +19,8 @@ import java.util.stream.Collectors; @Transactional(readOnly = true) public class JpaSupplierRepository implements SupplierRepository { + private static final Logger logger = LoggerFactory.getLogger(JpaSupplierRepository.class); + private final SupplierJpaRepository jpaRepository; private final SupplierMapper mapper; @@ -32,6 +36,7 @@ public class JpaSupplierRepository implements SupplierRepository { .map(mapper::toDomain); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findById", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -44,6 +49,7 @@ public class JpaSupplierRepository implements SupplierRepository { .collect(Collectors.toList()); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findAll", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -56,6 +62,7 @@ public class JpaSupplierRepository implements SupplierRepository { .collect(Collectors.toList()); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findByStatus", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -67,6 +74,7 @@ public class JpaSupplierRepository implements SupplierRepository { jpaRepository.save(mapper.toEntity(supplier)); return Result.success(null); } catch (Exception e) { + logger.trace("Database error in save", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -78,6 +86,7 @@ public class JpaSupplierRepository implements SupplierRepository { jpaRepository.deleteById(supplier.id().value()); return Result.success(null); } catch (Exception e) { + logger.trace("Database error in delete", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -87,6 +96,7 @@ public class JpaSupplierRepository implements SupplierRepository { try { return Result.success(jpaRepository.existsByName(name.value())); } catch (Exception e) { + logger.trace("Database error in existsByName", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } diff --git a/backend/src/main/java/de/effigenix/infrastructure/security/ApiAccessDeniedHandler.java b/backend/src/main/java/de/effigenix/infrastructure/security/ApiAccessDeniedHandler.java new file mode 100644 index 0000000..c7ed729 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/security/ApiAccessDeniedHandler.java @@ -0,0 +1,44 @@ +package de.effigenix.infrastructure.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.effigenix.infrastructure.usermanagement.web.dto.ErrorResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class ApiAccessDeniedHandler implements AccessDeniedHandler { + + private static final Logger logger = LoggerFactory.getLogger(ApiAccessDeniedHandler.class); + + private final ObjectMapper objectMapper; + + public ApiAccessDeniedHandler(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException { + logger.warn("Access denied for {}: {}", request.getRequestURI(), accessDeniedException.getMessage()); + + var errorResponse = ErrorResponse.from( + "FORBIDDEN", + "Access denied", + HttpStatus.FORBIDDEN.value(), + request.getRequestURI() + ); + + response.setStatus(HttpStatus.FORBIDDEN.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/security/ApiAuthenticationEntryPoint.java b/backend/src/main/java/de/effigenix/infrastructure/security/ApiAuthenticationEntryPoint.java new file mode 100644 index 0000000..ef880ee --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/security/ApiAuthenticationEntryPoint.java @@ -0,0 +1,44 @@ +package de.effigenix.infrastructure.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.effigenix.infrastructure.usermanagement.web.dto.ErrorResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class ApiAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private static final Logger logger = LoggerFactory.getLogger(ApiAuthenticationEntryPoint.class); + + private final ObjectMapper objectMapper; + + public ApiAuthenticationEntryPoint(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException { + logger.warn("Authentication failed for {}: {}", request.getRequestURI(), authException.getMessage()); + + var errorResponse = ErrorResponse.from( + "UNAUTHORIZED", + "Authentication required", + HttpStatus.UNAUTHORIZED.value(), + request.getRequestURI() + ); + + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/security/JwtAuthenticationFilter.java b/backend/src/main/java/de/effigenix/infrastructure/security/JwtAuthenticationFilter.java index fef3a12..8225c55 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/security/JwtAuthenticationFilter.java +++ b/backend/src/main/java/de/effigenix/infrastructure/security/JwtAuthenticationFilter.java @@ -101,13 +101,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { SecurityContextHolder.getContext().setAuthentication(authentication); } - } catch (JwtException | SecurityException e) { + } catch (JwtException | SecurityException | IllegalArgumentException e) { // Token validation failed - clear SecurityContext and continue // Spring Security will return 401 Unauthorized for protected endpoints SecurityContextHolder.clearContext(); - // Log the error for debugging - logger.debug("JWT authentication failed: " + e.getMessage(), e); + logger.warn("JWT authentication failed: " + e.getMessage()); } // Continue filter chain (even if authentication failed) diff --git a/backend/src/main/java/de/effigenix/infrastructure/security/SecurityConfig.java b/backend/src/main/java/de/effigenix/infrastructure/security/SecurityConfig.java index 800e16f..fc2fcc6 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/security/SecurityConfig.java +++ b/backend/src/main/java/de/effigenix/infrastructure/security/SecurityConfig.java @@ -39,9 +39,15 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final ApiAuthenticationEntryPoint authenticationEntryPoint; + private final ApiAccessDeniedHandler accessDeniedHandler; - public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, + ApiAuthenticationEntryPoint authenticationEntryPoint, + ApiAccessDeniedHandler accessDeniedHandler) { this.jwtAuthenticationFilter = jwtAuthenticationFilter; + this.authenticationEntryPoint = authenticationEntryPoint; + this.accessDeniedHandler = accessDeniedHandler; } /** @@ -92,26 +98,10 @@ public class SecurityConfig { .anyRequest().denyAll() ) - // Exception Handling: Return 401 Unauthorized for authentication failures + // Exception Handling: Return 401/403 with consistent ErrorResponse format .exceptionHandling(exception -> exception - .authenticationEntryPoint((request, response, authException) -> { - response.setStatus(401); - response.setContentType("application/json"); - response.getWriter().write( - "{\"error\":\"Unauthorized\",\"message\":\"" + - authException.getMessage() + - "\"}" - ); - }) - .accessDeniedHandler((request, response, accessDeniedException) -> { - response.setStatus(403); - response.setContentType("application/json"); - response.getWriter().write( - "{\"error\":\"Forbidden\",\"message\":\"" + - accessDeniedException.getMessage() + - "\"}" - ); - }) + .authenticationEntryPoint(authenticationEntryPoint) + .accessDeniedHandler(accessDeniedHandler) ) // Add JWT Authentication Filter before Spring Security's authentication filters diff --git a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaRoleRepository.java b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaRoleRepository.java index b7c159e..c71785a 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaRoleRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaRoleRepository.java @@ -10,6 +10,8 @@ import de.effigenix.shared.common.Result; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; import java.util.Optional; @@ -32,6 +34,8 @@ import java.util.stream.Collectors; @Transactional(readOnly = true) public class JpaRoleRepository implements RoleRepository { + private static final Logger logger = LoggerFactory.getLogger(JpaRoleRepository.class); + private final RoleJpaRepository jpaRepository; private final RoleMapper roleMapper; @@ -47,6 +51,7 @@ public class JpaRoleRepository implements RoleRepository { .map(roleMapper::toDomain); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findById", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -58,6 +63,7 @@ public class JpaRoleRepository implements RoleRepository { .map(roleMapper::toDomain); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findByName", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -70,6 +76,7 @@ public class JpaRoleRepository implements RoleRepository { .collect(Collectors.toList()); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findAll", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -81,6 +88,7 @@ public class JpaRoleRepository implements RoleRepository { jpaRepository.save(roleMapper.toEntity(role)); return Result.success(null); } catch (Exception e) { + logger.trace("Database error in save", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -92,6 +100,7 @@ public class JpaRoleRepository implements RoleRepository { jpaRepository.deleteById(role.id().value()); return Result.success(null); } catch (Exception e) { + logger.trace("Database error in delete", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -101,6 +110,7 @@ public class JpaRoleRepository implements RoleRepository { try { return Result.success(jpaRepository.existsByName(name)); } catch (Exception e) { + logger.trace("Database error in existsByName", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } diff --git a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaUserRepository.java b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaUserRepository.java index 32cb9b6..64ed314 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaUserRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaUserRepository.java @@ -10,6 +10,8 @@ import de.effigenix.shared.common.Result; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; import java.util.Optional; @@ -32,6 +34,7 @@ import java.util.stream.Collectors; @Transactional(readOnly = true) public class JpaUserRepository implements UserRepository { + private static final Logger logger = LoggerFactory.getLogger(JpaUserRepository.class); private final UserJpaRepository jpaRepository; private final UserMapper userMapper; @@ -47,6 +50,7 @@ public class JpaUserRepository implements UserRepository { .map(userMapper::toDomain); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findById", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -58,6 +62,7 @@ public class JpaUserRepository implements UserRepository { .map(userMapper::toDomain); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findByUsername", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -69,6 +74,7 @@ public class JpaUserRepository implements UserRepository { .map(userMapper::toDomain); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findByEmail", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -81,6 +87,7 @@ public class JpaUserRepository implements UserRepository { .collect(Collectors.toList()); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findByBranchId", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -93,6 +100,7 @@ public class JpaUserRepository implements UserRepository { .collect(Collectors.toList()); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findByStatus", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -105,6 +113,7 @@ public class JpaUserRepository implements UserRepository { .collect(Collectors.toList()); return Result.success(result); } catch (Exception e) { + logger.trace("Database error in findAll", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -116,6 +125,7 @@ public class JpaUserRepository implements UserRepository { jpaRepository.save(userMapper.toEntity(user)); return Result.success(null); } catch (Exception e) { + logger.trace("Database error in save", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -127,6 +137,7 @@ public class JpaUserRepository implements UserRepository { jpaRepository.deleteById(user.id().value()); return Result.success(null); } catch (Exception e) { + logger.trace("Database error in delete", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -136,6 +147,7 @@ public class JpaUserRepository implements UserRepository { try { return Result.success(jpaRepository.existsByUsername(username)); } catch (Exception e) { + logger.trace("Database error in existsByUsername", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } @@ -145,6 +157,7 @@ public class JpaUserRepository implements UserRepository { try { return Result.success(jpaRepository.existsByEmail(email)); } catch (Exception e) { + logger.trace("Database error in existsByEmail", e); return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); } } diff --git a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/AuthController.java b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/AuthController.java index 194c3b5..5d34e67 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/AuthController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/AuthController.java @@ -239,6 +239,7 @@ public class AuthController { return ResponseEntity.ok(LoginResponse.from(token)); } catch (RuntimeException ex) { logger.warn("Token refresh failed: {}", ex.getMessage()); + logger.trace("Token refresh exception details", ex); throw new AuthenticationFailedException( new UserError.InvalidCredentials() ); diff --git a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/RoleController.java b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/RoleController.java index 8cf7ea5..11aea38 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/RoleController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/RoleController.java @@ -103,30 +103,37 @@ public class RoleController { ActorId actorId = extractActorId(authentication); logger.info("Listing roles by actor: {}", actorId.value()); - return switch (roleRepository.findAll()) { - case Result.Failure> f -> { - logger.error("Failed to load roles: {}", f.error().message()); - yield ResponseEntity.internalServerError().build(); - } - case Result.Success> s -> { - List roles = s.value().stream() - .map(RoleDTO::from) - .collect(Collectors.toList()); - logger.info("Found {} roles", roles.size()); - yield ResponseEntity.ok(roles); - } - }; + var result = roleRepository.findAll(); + if (result.isFailure()) { + throw new RoleDomainErrorException(result.unsafeGetError()); + } + + List roles = result.unsafeGetValue().stream() + .map(RoleDTO::from) + .collect(Collectors.toList()); + logger.info("Found {} roles", roles.size()); + return ResponseEntity.ok(roles); } // ==================== Helper Methods ==================== - /** - * Extracts ActorId from Spring Security Authentication. - */ private ActorId extractActorId(Authentication authentication) { if (authentication == null || authentication.getName() == null) { throw new IllegalStateException("No authentication found in SecurityContext"); } return ActorId.of(authentication.getName()); } + + public static class RoleDomainErrorException extends RuntimeException { + private final RepositoryError error; + + public RoleDomainErrorException(RepositoryError error) { + super(error.message()); + this.error = error; + } + + public RepositoryError getError() { + return error; + } + } } diff --git a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/exception/GlobalExceptionHandler.java b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/exception/GlobalExceptionHandler.java index 8ecc169..cbf4a55 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/exception/GlobalExceptionHandler.java @@ -9,8 +9,10 @@ import de.effigenix.infrastructure.masterdata.web.controller.ArticleController; import de.effigenix.infrastructure.masterdata.web.controller.CustomerController; import de.effigenix.infrastructure.masterdata.web.controller.ProductCategoryController; import de.effigenix.infrastructure.masterdata.web.controller.SupplierController; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.infrastructure.masterdata.web.exception.MasterDataErrorHttpStatusMapper; import de.effigenix.infrastructure.usermanagement.web.controller.AuthController; +import de.effigenix.infrastructure.usermanagement.web.controller.RoleController; import de.effigenix.infrastructure.usermanagement.web.controller.UserController; import de.effigenix.infrastructure.usermanagement.web.dto.ErrorResponse; @@ -89,7 +91,7 @@ public class GlobalExceptionHandler { ) { UserError error = ex.getError(); int status = UserErrorHttpStatusMapper.toHttpStatus(error); - logger.warn("Domain error: {} - {}", error.code(), error.message()); + logDomainError("User", error.code(), error.message(), status); ErrorResponse errorResponse = ErrorResponse.from( error.code(), @@ -108,7 +110,7 @@ public class GlobalExceptionHandler { ) { ArticleError error = ex.getError(); int status = MasterDataErrorHttpStatusMapper.toHttpStatus(error); - logger.warn("Article domain error: {} - {}", error.code(), error.message()); + logDomainError("Article", error.code(), error.message(), status); ErrorResponse errorResponse = ErrorResponse.from( error.code(), @@ -127,7 +129,7 @@ public class GlobalExceptionHandler { ) { ProductCategoryError error = ex.getError(); int status = MasterDataErrorHttpStatusMapper.toHttpStatus(error); - logger.warn("ProductCategory domain error: {} - {}", error.code(), error.message()); + logDomainError("ProductCategory", error.code(), error.message(), status); ErrorResponse errorResponse = ErrorResponse.from( error.code(), @@ -146,7 +148,7 @@ public class GlobalExceptionHandler { ) { SupplierError error = ex.getError(); int status = MasterDataErrorHttpStatusMapper.toHttpStatus(error); - logger.warn("Supplier domain error: {} - {}", error.code(), error.message()); + logDomainError("Supplier", error.code(), error.message(), status); ErrorResponse errorResponse = ErrorResponse.from( error.code(), @@ -165,7 +167,7 @@ public class GlobalExceptionHandler { ) { CustomerError error = ex.getError(); int status = MasterDataErrorHttpStatusMapper.toHttpStatus(error); - logger.warn("Customer domain error: {} - {}", error.code(), error.message()); + logDomainError("Customer", error.code(), error.message(), status); ErrorResponse errorResponse = ErrorResponse.from( error.code(), @@ -177,6 +179,24 @@ public class GlobalExceptionHandler { return ResponseEntity.status(status).body(errorResponse); } + @ExceptionHandler(RoleController.RoleDomainErrorException.class) + public ResponseEntity handleRoleDomainError( + RoleController.RoleDomainErrorException ex, + HttpServletRequest request + ) { + RepositoryError error = ex.getError(); + logger.error("Role repository error: {}", error.message()); + + ErrorResponse errorResponse = ErrorResponse.from( + "REPOSITORY_ERROR", + error.message(), + HttpStatus.INTERNAL_SERVER_ERROR.value(), + request.getRequestURI() + ); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + /** * Handles validation errors from @Valid annotations. * @@ -325,7 +345,7 @@ public class GlobalExceptionHandler { RuntimeException ex, HttpServletRequest request ) { - logger.error("Unexpected error: {}", ex.getMessage(), ex); + logger.error("Unexpected runtime error: {}", ex.getMessage(), ex); ErrorResponse errorResponse = ErrorResponse.from( "INTERNAL_ERROR", @@ -338,4 +358,31 @@ public class GlobalExceptionHandler { .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(errorResponse); } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException( + Exception ex, + HttpServletRequest request + ) { + logger.error("Unexpected checked exception: {}", ex.getMessage(), ex); + + ErrorResponse errorResponse = ErrorResponse.from( + "INTERNAL_ERROR", + "An unexpected error occurred. Please contact support.", + HttpStatus.INTERNAL_SERVER_ERROR.value(), + request.getRequestURI() + ); + + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorResponse); + } + + private void logDomainError(String context, String code, String message, int status) { + if (status >= 500) { + logger.error("{} domain error: {} - {}", context, code, message); + } else { + logger.warn("{} domain error: {} - {}", context, code, message); + } + } }