1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 11:59:35 +01:00

fix(backend): lückenloses Error-Handling und Logging im Infrastructure Layer

Stack-Traces in allen JPA-Repositories per logger.trace() bewahren, bevor
sie durch Result.failure() auf die Message reduziert werden. Security-Layer
erhält eigene Handler-Beans (ApiAuthenticationEntryPoint, ApiAccessDeniedHandler)
mit konsistentem ErrorResponse-Format statt Inline-Lambdas. JWT-Filter loggt
Validierungsfehler auf WARN statt DEBUG und fängt IllegalArgumentException.
RoleController nutzt jetzt das Exception-Pattern der anderen Controller statt
eines leeren 500-Bodys. GlobalExceptionHandler differenziert zwischen
fachlichen Domain-Fehlern (WARN) und technischen Repository-Fehlern (ERROR)
und fängt auch checked Exceptions als Catch-All.
This commit is contained in:
Sebastian Frick 2026-02-18 23:23:00 +01:00
parent fbed3f899f
commit e4f0665086
13 changed files with 241 additions and 45 deletions

View file

@ -4,6 +4,8 @@ import de.effigenix.domain.masterdata.*;
import de.effigenix.infrastructure.masterdata.persistence.mapper.ArticleMapper; import de.effigenix.infrastructure.masterdata.persistence.mapper.ArticleMapper;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -17,6 +19,8 @@ import java.util.stream.Collectors;
@Transactional(readOnly = true) @Transactional(readOnly = true)
public class JpaArticleRepository implements ArticleRepository { public class JpaArticleRepository implements ArticleRepository {
private static final Logger logger = LoggerFactory.getLogger(JpaArticleRepository.class);
private final ArticleJpaRepository jpaRepository; private final ArticleJpaRepository jpaRepository;
private final ArticleMapper mapper; private final ArticleMapper mapper;
@ -32,6 +36,7 @@ public class JpaArticleRepository implements ArticleRepository {
.map(mapper::toDomain); .map(mapper::toDomain);
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findById", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -44,6 +49,7 @@ public class JpaArticleRepository implements ArticleRepository {
.collect(Collectors.toList()); .collect(Collectors.toList());
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findAll", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -56,6 +62,7 @@ public class JpaArticleRepository implements ArticleRepository {
.collect(Collectors.toList()); .collect(Collectors.toList());
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findByCategory", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -68,6 +75,7 @@ public class JpaArticleRepository implements ArticleRepository {
.collect(Collectors.toList()); .collect(Collectors.toList());
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findByStatus", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -79,6 +87,7 @@ public class JpaArticleRepository implements ArticleRepository {
jpaRepository.save(mapper.toEntity(article)); jpaRepository.save(mapper.toEntity(article));
return Result.success(null); return Result.success(null);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in save", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -90,6 +99,7 @@ public class JpaArticleRepository implements ArticleRepository {
jpaRepository.deleteById(article.id().value()); jpaRepository.deleteById(article.id().value());
return Result.success(null); return Result.success(null);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in delete", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -99,6 +109,7 @@ public class JpaArticleRepository implements ArticleRepository {
try { try {
return Result.success(jpaRepository.existsByArticleNumber(articleNumber.value())); return Result.success(jpaRepository.existsByArticleNumber(articleNumber.value()));
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in existsByArticleNumber", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }

View file

@ -5,6 +5,8 @@ import de.effigenix.infrastructure.masterdata.persistence.entity.FrameContractEn
import de.effigenix.infrastructure.masterdata.persistence.mapper.CustomerMapper; import de.effigenix.infrastructure.masterdata.persistence.mapper.CustomerMapper;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -18,6 +20,8 @@ import java.util.stream.Collectors;
@Transactional(readOnly = true) @Transactional(readOnly = true)
public class JpaCustomerRepository implements CustomerRepository { public class JpaCustomerRepository implements CustomerRepository {
private static final Logger logger = LoggerFactory.getLogger(JpaCustomerRepository.class);
private final CustomerJpaRepository jpaRepository; private final CustomerJpaRepository jpaRepository;
private final FrameContractJpaRepository frameContractJpaRepository; private final FrameContractJpaRepository frameContractJpaRepository;
private final CustomerMapper mapper; private final CustomerMapper mapper;
@ -40,6 +44,7 @@ public class JpaCustomerRepository implements CustomerRepository {
var fcEntity = frameContractJpaRepository.findByCustomerId(id.value()).orElse(null); var fcEntity = frameContractJpaRepository.findByCustomerId(id.value()).orElse(null);
return Result.success(Optional.of(mapper.toDomain(entityOpt.get(), fcEntity))); return Result.success(Optional.of(mapper.toDomain(entityOpt.get(), fcEntity)));
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findById", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -55,6 +60,7 @@ public class JpaCustomerRepository implements CustomerRepository {
.collect(Collectors.toList()); .collect(Collectors.toList());
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findAll", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -70,6 +76,7 @@ public class JpaCustomerRepository implements CustomerRepository {
.collect(Collectors.toList()); .collect(Collectors.toList());
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findByType", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -85,6 +92,7 @@ public class JpaCustomerRepository implements CustomerRepository {
.collect(Collectors.toList()); .collect(Collectors.toList());
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findByStatus", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -104,6 +112,7 @@ public class JpaCustomerRepository implements CustomerRepository {
return Result.success(null); return Result.success(null);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in save", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -116,6 +125,7 @@ public class JpaCustomerRepository implements CustomerRepository {
jpaRepository.deleteById(customer.id().value()); jpaRepository.deleteById(customer.id().value());
return Result.success(null); return Result.success(null);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in delete", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -125,6 +135,7 @@ public class JpaCustomerRepository implements CustomerRepository {
try { try {
return Result.success(jpaRepository.existsByName(name.value())); return Result.success(jpaRepository.existsByName(name.value()));
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in existsByName", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }

View file

@ -7,6 +7,8 @@ import de.effigenix.domain.masterdata.ProductCategoryRepository;
import de.effigenix.infrastructure.masterdata.persistence.mapper.ProductCategoryMapper; import de.effigenix.infrastructure.masterdata.persistence.mapper.ProductCategoryMapper;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -20,6 +22,8 @@ import java.util.stream.Collectors;
@Transactional(readOnly = true) @Transactional(readOnly = true)
public class JpaProductCategoryRepository implements ProductCategoryRepository { public class JpaProductCategoryRepository implements ProductCategoryRepository {
private static final Logger logger = LoggerFactory.getLogger(JpaProductCategoryRepository.class);
private final ProductCategoryJpaRepository jpaRepository; private final ProductCategoryJpaRepository jpaRepository;
private final ProductCategoryMapper mapper; private final ProductCategoryMapper mapper;
@ -35,6 +39,7 @@ public class JpaProductCategoryRepository implements ProductCategoryRepository {
.map(mapper::toDomain); .map(mapper::toDomain);
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findById", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -47,6 +52,7 @@ public class JpaProductCategoryRepository implements ProductCategoryRepository {
.collect(Collectors.toList()); .collect(Collectors.toList());
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findAll", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -58,6 +64,7 @@ public class JpaProductCategoryRepository implements ProductCategoryRepository {
jpaRepository.save(mapper.toEntity(category)); jpaRepository.save(mapper.toEntity(category));
return Result.success(null); return Result.success(null);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in save", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -69,6 +76,7 @@ public class JpaProductCategoryRepository implements ProductCategoryRepository {
jpaRepository.deleteById(category.id().value()); jpaRepository.deleteById(category.id().value());
return Result.success(null); return Result.success(null);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in delete", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -78,6 +86,7 @@ public class JpaProductCategoryRepository implements ProductCategoryRepository {
try { try {
return Result.success(jpaRepository.existsByName(name.value())); return Result.success(jpaRepository.existsByName(name.value()));
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in existsByName", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }

View file

@ -4,6 +4,8 @@ import de.effigenix.domain.masterdata.*;
import de.effigenix.infrastructure.masterdata.persistence.mapper.SupplierMapper; import de.effigenix.infrastructure.masterdata.persistence.mapper.SupplierMapper;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -17,6 +19,8 @@ import java.util.stream.Collectors;
@Transactional(readOnly = true) @Transactional(readOnly = true)
public class JpaSupplierRepository implements SupplierRepository { public class JpaSupplierRepository implements SupplierRepository {
private static final Logger logger = LoggerFactory.getLogger(JpaSupplierRepository.class);
private final SupplierJpaRepository jpaRepository; private final SupplierJpaRepository jpaRepository;
private final SupplierMapper mapper; private final SupplierMapper mapper;
@ -32,6 +36,7 @@ public class JpaSupplierRepository implements SupplierRepository {
.map(mapper::toDomain); .map(mapper::toDomain);
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findById", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -44,6 +49,7 @@ public class JpaSupplierRepository implements SupplierRepository {
.collect(Collectors.toList()); .collect(Collectors.toList());
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findAll", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -56,6 +62,7 @@ public class JpaSupplierRepository implements SupplierRepository {
.collect(Collectors.toList()); .collect(Collectors.toList());
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findByStatus", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -67,6 +74,7 @@ public class JpaSupplierRepository implements SupplierRepository {
jpaRepository.save(mapper.toEntity(supplier)); jpaRepository.save(mapper.toEntity(supplier));
return Result.success(null); return Result.success(null);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in save", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -78,6 +86,7 @@ public class JpaSupplierRepository implements SupplierRepository {
jpaRepository.deleteById(supplier.id().value()); jpaRepository.deleteById(supplier.id().value());
return Result.success(null); return Result.success(null);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in delete", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -87,6 +96,7 @@ public class JpaSupplierRepository implements SupplierRepository {
try { try {
return Result.success(jpaRepository.existsByName(name.value())); return Result.success(jpaRepository.existsByName(name.value()));
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in existsByName", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -101,13 +101,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
} }
} catch (JwtException | SecurityException e) { } catch (JwtException | SecurityException | IllegalArgumentException e) {
// Token validation failed - clear SecurityContext and continue // Token validation failed - clear SecurityContext and continue
// Spring Security will return 401 Unauthorized for protected endpoints // Spring Security will return 401 Unauthorized for protected endpoints
SecurityContextHolder.clearContext(); SecurityContextHolder.clearContext();
// Log the error for debugging logger.warn("JWT authentication failed: " + e.getMessage());
logger.debug("JWT authentication failed: " + e.getMessage(), e);
} }
// Continue filter chain (even if authentication failed) // Continue filter chain (even if authentication failed)

View file

@ -39,9 +39,15 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
public class SecurityConfig { public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter; 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.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.authenticationEntryPoint = authenticationEntryPoint;
this.accessDeniedHandler = accessDeniedHandler;
} }
/** /**
@ -92,26 +98,10 @@ public class SecurityConfig {
.anyRequest().denyAll() .anyRequest().denyAll()
) )
// Exception Handling: Return 401 Unauthorized for authentication failures // Exception Handling: Return 401/403 with consistent ErrorResponse format
.exceptionHandling(exception -> exception .exceptionHandling(exception -> exception
.authenticationEntryPoint((request, response, authException) -> { .authenticationEntryPoint(authenticationEntryPoint)
response.setStatus(401); .accessDeniedHandler(accessDeniedHandler)
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() +
"\"}"
);
})
) )
// Add JWT Authentication Filter before Spring Security's authentication filters // Add JWT Authentication Filter before Spring Security's authentication filters

View file

@ -10,6 +10,8 @@ import de.effigenix.shared.common.Result;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -32,6 +34,8 @@ import java.util.stream.Collectors;
@Transactional(readOnly = true) @Transactional(readOnly = true)
public class JpaRoleRepository implements RoleRepository { public class JpaRoleRepository implements RoleRepository {
private static final Logger logger = LoggerFactory.getLogger(JpaRoleRepository.class);
private final RoleJpaRepository jpaRepository; private final RoleJpaRepository jpaRepository;
private final RoleMapper roleMapper; private final RoleMapper roleMapper;
@ -47,6 +51,7 @@ public class JpaRoleRepository implements RoleRepository {
.map(roleMapper::toDomain); .map(roleMapper::toDomain);
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findById", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -58,6 +63,7 @@ public class JpaRoleRepository implements RoleRepository {
.map(roleMapper::toDomain); .map(roleMapper::toDomain);
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findByName", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -70,6 +76,7 @@ public class JpaRoleRepository implements RoleRepository {
.collect(Collectors.toList()); .collect(Collectors.toList());
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findAll", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -81,6 +88,7 @@ public class JpaRoleRepository implements RoleRepository {
jpaRepository.save(roleMapper.toEntity(role)); jpaRepository.save(roleMapper.toEntity(role));
return Result.success(null); return Result.success(null);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in save", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -92,6 +100,7 @@ public class JpaRoleRepository implements RoleRepository {
jpaRepository.deleteById(role.id().value()); jpaRepository.deleteById(role.id().value());
return Result.success(null); return Result.success(null);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in delete", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -101,6 +110,7 @@ public class JpaRoleRepository implements RoleRepository {
try { try {
return Result.success(jpaRepository.existsByName(name)); return Result.success(jpaRepository.existsByName(name));
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in existsByName", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }

View file

@ -10,6 +10,8 @@ import de.effigenix.shared.common.Result;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -32,6 +34,7 @@ import java.util.stream.Collectors;
@Transactional(readOnly = true) @Transactional(readOnly = true)
public class JpaUserRepository implements UserRepository { public class JpaUserRepository implements UserRepository {
private static final Logger logger = LoggerFactory.getLogger(JpaUserRepository.class);
private final UserJpaRepository jpaRepository; private final UserJpaRepository jpaRepository;
private final UserMapper userMapper; private final UserMapper userMapper;
@ -47,6 +50,7 @@ public class JpaUserRepository implements UserRepository {
.map(userMapper::toDomain); .map(userMapper::toDomain);
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findById", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -58,6 +62,7 @@ public class JpaUserRepository implements UserRepository {
.map(userMapper::toDomain); .map(userMapper::toDomain);
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findByUsername", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -69,6 +74,7 @@ public class JpaUserRepository implements UserRepository {
.map(userMapper::toDomain); .map(userMapper::toDomain);
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findByEmail", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -81,6 +87,7 @@ public class JpaUserRepository implements UserRepository {
.collect(Collectors.toList()); .collect(Collectors.toList());
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findByBranchId", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -93,6 +100,7 @@ public class JpaUserRepository implements UserRepository {
.collect(Collectors.toList()); .collect(Collectors.toList());
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findByStatus", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -105,6 +113,7 @@ public class JpaUserRepository implements UserRepository {
.collect(Collectors.toList()); .collect(Collectors.toList());
return Result.success(result); return Result.success(result);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findAll", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -116,6 +125,7 @@ public class JpaUserRepository implements UserRepository {
jpaRepository.save(userMapper.toEntity(user)); jpaRepository.save(userMapper.toEntity(user));
return Result.success(null); return Result.success(null);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in save", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -127,6 +137,7 @@ public class JpaUserRepository implements UserRepository {
jpaRepository.deleteById(user.id().value()); jpaRepository.deleteById(user.id().value());
return Result.success(null); return Result.success(null);
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in delete", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -136,6 +147,7 @@ public class JpaUserRepository implements UserRepository {
try { try {
return Result.success(jpaRepository.existsByUsername(username)); return Result.success(jpaRepository.existsByUsername(username));
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in existsByUsername", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }
@ -145,6 +157,7 @@ public class JpaUserRepository implements UserRepository {
try { try {
return Result.success(jpaRepository.existsByEmail(email)); return Result.success(jpaRepository.existsByEmail(email));
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in existsByEmail", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }

View file

@ -239,6 +239,7 @@ public class AuthController {
return ResponseEntity.ok(LoginResponse.from(token)); return ResponseEntity.ok(LoginResponse.from(token));
} catch (RuntimeException ex) { } catch (RuntimeException ex) {
logger.warn("Token refresh failed: {}", ex.getMessage()); logger.warn("Token refresh failed: {}", ex.getMessage());
logger.trace("Token refresh exception details", ex);
throw new AuthenticationFailedException( throw new AuthenticationFailedException(
new UserError.InvalidCredentials() new UserError.InvalidCredentials()
); );

View file

@ -103,30 +103,37 @@ public class RoleController {
ActorId actorId = extractActorId(authentication); ActorId actorId = extractActorId(authentication);
logger.info("Listing roles by actor: {}", actorId.value()); logger.info("Listing roles by actor: {}", actorId.value());
return switch (roleRepository.findAll()) { var result = roleRepository.findAll();
case Result.Failure<RepositoryError, List<Role>> f -> { if (result.isFailure()) {
logger.error("Failed to load roles: {}", f.error().message()); throw new RoleDomainErrorException(result.unsafeGetError());
yield ResponseEntity.internalServerError().build();
} }
case Result.Success<RepositoryError, List<Role>> s -> {
List<RoleDTO> roles = s.value().stream() List<RoleDTO> roles = result.unsafeGetValue().stream()
.map(RoleDTO::from) .map(RoleDTO::from)
.collect(Collectors.toList()); .collect(Collectors.toList());
logger.info("Found {} roles", roles.size()); logger.info("Found {} roles", roles.size());
yield ResponseEntity.ok(roles); return ResponseEntity.ok(roles);
}
};
} }
// ==================== Helper Methods ==================== // ==================== Helper Methods ====================
/**
* Extracts ActorId from Spring Security Authentication.
*/
private ActorId extractActorId(Authentication authentication) { private ActorId extractActorId(Authentication authentication) {
if (authentication == null || authentication.getName() == null) { if (authentication == null || authentication.getName() == null) {
throw new IllegalStateException("No authentication found in SecurityContext"); throw new IllegalStateException("No authentication found in SecurityContext");
} }
return ActorId.of(authentication.getName()); 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;
}
}
} }

View file

@ -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.CustomerController;
import de.effigenix.infrastructure.masterdata.web.controller.ProductCategoryController; import de.effigenix.infrastructure.masterdata.web.controller.ProductCategoryController;
import de.effigenix.infrastructure.masterdata.web.controller.SupplierController; 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.masterdata.web.exception.MasterDataErrorHttpStatusMapper;
import de.effigenix.infrastructure.usermanagement.web.controller.AuthController; 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.controller.UserController;
import de.effigenix.infrastructure.usermanagement.web.dto.ErrorResponse; import de.effigenix.infrastructure.usermanagement.web.dto.ErrorResponse;
@ -89,7 +91,7 @@ public class GlobalExceptionHandler {
) { ) {
UserError error = ex.getError(); UserError error = ex.getError();
int status = UserErrorHttpStatusMapper.toHttpStatus(error); int status = UserErrorHttpStatusMapper.toHttpStatus(error);
logger.warn("Domain error: {} - {}", error.code(), error.message()); logDomainError("User", error.code(), error.message(), status);
ErrorResponse errorResponse = ErrorResponse.from( ErrorResponse errorResponse = ErrorResponse.from(
error.code(), error.code(),
@ -108,7 +110,7 @@ public class GlobalExceptionHandler {
) { ) {
ArticleError error = ex.getError(); ArticleError error = ex.getError();
int status = MasterDataErrorHttpStatusMapper.toHttpStatus(error); 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( ErrorResponse errorResponse = ErrorResponse.from(
error.code(), error.code(),
@ -127,7 +129,7 @@ public class GlobalExceptionHandler {
) { ) {
ProductCategoryError error = ex.getError(); ProductCategoryError error = ex.getError();
int status = MasterDataErrorHttpStatusMapper.toHttpStatus(error); 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( ErrorResponse errorResponse = ErrorResponse.from(
error.code(), error.code(),
@ -146,7 +148,7 @@ public class GlobalExceptionHandler {
) { ) {
SupplierError error = ex.getError(); SupplierError error = ex.getError();
int status = MasterDataErrorHttpStatusMapper.toHttpStatus(error); 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( ErrorResponse errorResponse = ErrorResponse.from(
error.code(), error.code(),
@ -165,7 +167,7 @@ public class GlobalExceptionHandler {
) { ) {
CustomerError error = ex.getError(); CustomerError error = ex.getError();
int status = MasterDataErrorHttpStatusMapper.toHttpStatus(error); 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( ErrorResponse errorResponse = ErrorResponse.from(
error.code(), error.code(),
@ -177,6 +179,24 @@ public class GlobalExceptionHandler {
return ResponseEntity.status(status).body(errorResponse); return ResponseEntity.status(status).body(errorResponse);
} }
@ExceptionHandler(RoleController.RoleDomainErrorException.class)
public ResponseEntity<ErrorResponse> 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. * Handles validation errors from @Valid annotations.
* *
@ -325,7 +345,7 @@ public class GlobalExceptionHandler {
RuntimeException ex, RuntimeException ex,
HttpServletRequest request HttpServletRequest request
) { ) {
logger.error("Unexpected error: {}", ex.getMessage(), ex); logger.error("Unexpected runtime error: {}", ex.getMessage(), ex);
ErrorResponse errorResponse = ErrorResponse.from( ErrorResponse errorResponse = ErrorResponse.from(
"INTERNAL_ERROR", "INTERNAL_ERROR",
@ -338,4 +358,31 @@ public class GlobalExceptionHandler {
.status(HttpStatus.INTERNAL_SERVER_ERROR) .status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorResponse); .body(errorResponse);
} }
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> 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);
}
}
} }