Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
* Main Spring Boot application entry point for AssetTrack.
*/
@SpringBootApplication
@EnableScheduling
@EnableConfigurationProperties(RsaKeyProperties.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,118 +21,146 @@
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;

/**
* Global exception handler that converts backend exceptions into ApiError
* responses.
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

private ApiError buildApiError(HttpStatus status, String message, String path) {
return ApiError.builder()
.timestamp(Instant.now().toString())
.status(status.value())
.error(status.getReasonPhrase())
.message(message)
.path(path)
.build();
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleValidationExceptions(MethodArgumentNotValidException ex, HttpServletRequest request) {
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors().stream()
.map(error -> FieldError.builder()
.field(error.getField())
.message(error.getDefaultMessage())
.build())
.collect(Collectors.toList());

ApiError apiError = ApiError.builder()
.timestamp(Instant.now().toString())
.status(HttpStatus.BAD_REQUEST.value())
.error(HttpStatus.BAD_REQUEST.getReasonPhrase())
.message("Validation failed")
.path(request.getRequestURI())
.fieldErrors(fieldErrors)
.build();

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(apiError);
}

@ExceptionHandler({NoHandlerFoundException.class, NoResourceFoundException.class, com.assettrack.exception.ResourceNotFoundException.class})
public ResponseEntity<ApiError> handleNotFound(Exception ex, HttpServletRequest request) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(buildApiError(HttpStatus.NOT_FOUND, ex.getMessage(), request.getRequestURI()));
}

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiError> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpServletRequest request) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(buildApiError(HttpStatus.BAD_REQUEST, "Malformed JSON request", request.getRequestURI()));
}

@ExceptionHandler(EmailAlreadyExistsException.class)
public ResponseEntity<ApiError> handleEmailExists(EmailAlreadyExistsException ex, HttpServletRequest request) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(buildApiError(HttpStatus.CONFLICT, ex.getMessage(), request.getRequestURI()));
}

@ExceptionHandler({ConflictException.class})
public ResponseEntity<ApiError> handleBadRequest(RuntimeException ex, HttpServletRequest request) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(buildApiError(HttpStatus.BAD_REQUEST, ex.getMessage(), request.getRequestURI()));
}

@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ApiError> handleBadCredentials(BadCredentialsException ex, HttpServletRequest request) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(buildApiError(HttpStatus.UNAUTHORIZED, "Invalid email or password", request.getRequestURI()));
}

@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiError> handleAccessDenied(AccessDeniedException ex, HttpServletRequest request) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(buildApiError(HttpStatus.FORBIDDEN, "Access denied", request.getRequestURI()));
}

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ApiError> handleTypeMismatch(MethodArgumentTypeMismatchException ex, HttpServletRequest request) {
String expectedType = ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "Unknown";

FieldError fieldError = FieldError.builder()
.field(ex.getName())
.message(String.format("Type mismatch for parameter: expected type %s", expectedType))
.rejectedValue(ex.getValue())
.build();

ApiError apiError = ApiError.builder()
.timestamp(Instant.now().toString())
.status(HttpStatus.BAD_REQUEST.value())
.error(HttpStatus.BAD_REQUEST.getReasonPhrase())
.message("Type mismatch")
.path(request.getRequestURI())
.fieldErrors(Collections.singletonList(fieldError))
.build();

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(apiError);
}

@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ApiError> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpServletRequest request) {
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(buildApiError(HttpStatus.METHOD_NOT_ALLOWED, ex.getMessage(), request.getRequestURI()));
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleAllExceptions(Exception ex, HttpServletRequest request) {
if (ex instanceof com.assettrack.exception.BaseException baseException) {
HttpStatus status = HttpStatus.resolve(baseException.getStatusCode());
if (status == null) {
status = HttpStatus.INTERNAL_SERVER_ERROR;
}

return ResponseEntity.status(status)
.body(buildApiError(status, baseException.getMessage(), request.getRequestURI()));
private ApiError buildApiError(HttpStatus status, String message, String path) {
return ApiError.builder()
.timestamp(Instant.now().toString())
.status(status.value())
.error(status.getReasonPhrase())
.message(message)
.path(path)
.build();
}

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(buildApiError(HttpStatus.INTERNAL_SERVER_ERROR, "An unexpected error occurred", request.getRequestURI()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleValidationExceptions(MethodArgumentNotValidException ex,
HttpServletRequest request) {
log.warn("Validation failed for {}: {}", request.getRequestURI(), ex.getMessage());
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors().stream()
.map(error -> FieldError.builder()
.field(error.getField())
.message(error.getDefaultMessage())
.build())
.collect(Collectors.toList());

ApiError apiError = ApiError.builder()
.timestamp(Instant.now().toString())
.status(HttpStatus.BAD_REQUEST.value())
.error(HttpStatus.BAD_REQUEST.getReasonPhrase())
.message("Validation failed")
.path(request.getRequestURI())
.fieldErrors(fieldErrors)
.build();

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(apiError);
}

@ExceptionHandler({ NoHandlerFoundException.class, NoResourceFoundException.class,
com.assettrack.exception.ResourceNotFoundException.class })
public ResponseEntity<ApiError> handleNotFound(Exception ex, HttpServletRequest request) {
log.warn("Not found at {}: {}", request.getRequestURI(), ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(buildApiError(HttpStatus.NOT_FOUND, ex.getMessage(), request.getRequestURI()));
}

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiError> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
HttpServletRequest request) {
log.warn("Malformed JSON at {}", request.getRequestURI(), ex);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(buildApiError(HttpStatus.BAD_REQUEST, "Malformed JSON request",
request.getRequestURI()));
}

@ExceptionHandler(EmailAlreadyExistsException.class)
public ResponseEntity<ApiError> handleEmailExists(EmailAlreadyExistsException ex, HttpServletRequest request) {
log.warn("Conflict at {}: {}", request.getRequestURI(), ex.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(buildApiError(HttpStatus.CONFLICT, ex.getMessage(), request.getRequestURI()));
}

@ExceptionHandler({ ConflictException.class })
public ResponseEntity<ApiError> handleBadRequest(RuntimeException ex, HttpServletRequest request) {
log.warn("Bad request at {}: {}", request.getRequestURI(), ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(buildApiError(HttpStatus.BAD_REQUEST, ex.getMessage(), request.getRequestURI()));
}

@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ApiError> handleBadCredentials(BadCredentialsException ex, HttpServletRequest request) {
log.warn("Unauthorized access at {}", request.getRequestURI());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(buildApiError(HttpStatus.UNAUTHORIZED, "Invalid email or password",
request.getRequestURI()));
}

@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiError> handleAccessDenied(AccessDeniedException ex, HttpServletRequest request) {
log.warn("Access denied at {}", request.getRequestURI());
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(buildApiError(HttpStatus.FORBIDDEN, "Access denied", request.getRequestURI()));
}

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ApiError> handleTypeMismatch(MethodArgumentTypeMismatchException ex,
HttpServletRequest request) {
String expectedType = ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "Unknown";

FieldError fieldError = FieldError.builder()
.field(ex.getName())
.message(String.format("Type mismatch for parameter: expected type %s", expectedType))
.rejectedValue(ex.getValue())
.build();

ApiError apiError = ApiError.builder()
.timestamp(Instant.now().toString())
.status(HttpStatus.BAD_REQUEST.value())
.error(HttpStatus.BAD_REQUEST.getReasonPhrase())
.message("Type mismatch")
.path(request.getRequestURI())
.fieldErrors(Collections.singletonList(fieldError))
.build();

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(apiError);
}

@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ApiError> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex,
HttpServletRequest request) {
log.warn("Method not supported at {}: {}", request.getRequestURI(), ex.getMethod());
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(buildApiError(HttpStatus.METHOD_NOT_ALLOWED, ex.getMessage(),
request.getRequestURI()));
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleAllExceptions(Exception ex, HttpServletRequest request) {
if (ex instanceof com.assettrack.exception.BaseException baseException) {
HttpStatus status = HttpStatus.resolve(baseException.getStatusCode());
if (status == null) {
status = HttpStatus.INTERNAL_SERVER_ERROR;
}

log.warn("Application exception at {}: {}", request.getRequestURI(),
baseException.getMessage());

return ResponseEntity.status(status)
.body(buildApiError(status, baseException.getMessage(),
request.getRequestURI()));
}

log.error("Unexpected exception at {}", request.getRequestURI(), ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(buildApiError(HttpStatus.INTERNAL_SERVER_ERROR, "An unexpected error occurred",
request.getRequestURI()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

import java.util.List;

/**
* Standard error envelope returned by the global exception handler.
*/
@Data
@Builder
@NoArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* Legacy error response envelope kept for backward compatibility.
*/
@Data
@Builder
@NoArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package com.assettrack.common.exception.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* Validation failure details for a single field.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class FieldError {
private String field;
private String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* Detailed validation error entry used by legacy responses.
*/
@Data
@Builder
@NoArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

import java.util.List;

/**
* Legacy validation error response envelope.
*/
@Data
@Builder
@NoArgsConstructor
Expand Down
57 changes: 31 additions & 26 deletions backend/src/main/java/com/assettrack/config/OpenApiConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,38 @@

import java.util.List;

/**
* OpenAPI configuration for the AssetTrack API documentation.
*/
@Configuration
public class OpenApiConfig {

@Bean
public OpenAPI assetTrackOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("AssetTrack API")
.description("IT Asset Management and Tracking System API. " +
"Provides endpoints for managing assets, allocations, users, " +
"authentication, dashboard analytics, and notifications.")
.version("0.1.0")
.contact(new Contact()
.name("AssetTrack Team")
.email("admin@assettrack.com"))
.license(new License()
.name("MIT License")
.url("https://opensource.org/licenses/MIT")))
.servers(List.of(
new Server().url("http://localhost:8080").description("Local Development")))
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
.components(new Components()
.addSecuritySchemes("Bearer Authentication",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("Enter your JWT token")));
}
@Bean
public OpenAPI assetTrackOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("AssetTrack API")
.description("IT Asset Management and Tracking System API. " +
"Provides endpoints for managing assets, allocations, users, "
+
"authentication, dashboard analytics, and notifications.")
.version("0.1.0")
.contact(new Contact()
.name("AssetTrack Team")
.email("admin@assettrack.com"))
.license(new License()
.name("MIT License")
.url("https://opensource.org/licenses/MIT")))
.servers(List.of(
new Server().url("http://localhost:8080")
.description("Local Development")))
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
.components(new Components()
.addSecuritySchemes("Bearer Authentication",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("Enter your JWT token")));
}
}
Loading
Loading