diff --git a/backend/src/main/java/com/assettrack/AssetTrackApplication.java b/backend/src/main/java/com/assettrack/AssetTrackApplication.java index e988abc..d587742 100644 --- a/backend/src/main/java/com/assettrack/AssetTrackApplication.java +++ b/backend/src/main/java/com/assettrack/AssetTrackApplication.java @@ -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) diff --git a/backend/src/main/java/com/assettrack/common/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/assettrack/common/exception/GlobalExceptionHandler.java index c3a8fd2..7542155 100644 --- a/backend/src/main/java/com/assettrack/common/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/assettrack/common/exception/GlobalExceptionHandler.java @@ -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 handleValidationExceptions(MethodArgumentNotValidException ex, HttpServletRequest request) { - List 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 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 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 handleEmailExists(EmailAlreadyExistsException ex, HttpServletRequest request) { - return ResponseEntity.status(HttpStatus.CONFLICT) - .body(buildApiError(HttpStatus.CONFLICT, ex.getMessage(), request.getRequestURI())); - } - - @ExceptionHandler({ConflictException.class}) - public ResponseEntity 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 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 handleAccessDenied(AccessDeniedException ex, HttpServletRequest request) { - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(buildApiError(HttpStatus.FORBIDDEN, "Access denied", request.getRequestURI())); - } - - @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity 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 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 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 handleValidationExceptions(MethodArgumentNotValidException ex, + HttpServletRequest request) { + log.warn("Validation failed for {}: {}", request.getRequestURI(), ex.getMessage()); + List 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 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 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 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 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 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 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 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 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 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())); + } } diff --git a/backend/src/main/java/com/assettrack/common/exception/dto/ApiError.java b/backend/src/main/java/com/assettrack/common/exception/dto/ApiError.java index b7b60b9..09f8e9d 100644 --- a/backend/src/main/java/com/assettrack/common/exception/dto/ApiError.java +++ b/backend/src/main/java/com/assettrack/common/exception/dto/ApiError.java @@ -8,6 +8,9 @@ import java.util.List; +/** + * Standard error envelope returned by the global exception handler. + */ @Data @Builder @NoArgsConstructor diff --git a/backend/src/main/java/com/assettrack/common/exception/dto/ErrorResponse.java b/backend/src/main/java/com/assettrack/common/exception/dto/ErrorResponse.java index 49cf6a1..bddc613 100644 --- a/backend/src/main/java/com/assettrack/common/exception/dto/ErrorResponse.java +++ b/backend/src/main/java/com/assettrack/common/exception/dto/ErrorResponse.java @@ -5,6 +5,9 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Legacy error response envelope kept for backward compatibility. + */ @Data @Builder @NoArgsConstructor diff --git a/backend/src/main/java/com/assettrack/common/exception/dto/FieldError.java b/backend/src/main/java/com/assettrack/common/exception/dto/FieldError.java index 0c441b3..16e5b25 100644 --- a/backend/src/main/java/com/assettrack/common/exception/dto/FieldError.java +++ b/backend/src/main/java/com/assettrack/common/exception/dto/FieldError.java @@ -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; diff --git a/backend/src/main/java/com/assettrack/common/exception/dto/ValidationErrorDetail.java b/backend/src/main/java/com/assettrack/common/exception/dto/ValidationErrorDetail.java index 636fdd4..ff024d4 100644 --- a/backend/src/main/java/com/assettrack/common/exception/dto/ValidationErrorDetail.java +++ b/backend/src/main/java/com/assettrack/common/exception/dto/ValidationErrorDetail.java @@ -5,6 +5,9 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Detailed validation error entry used by legacy responses. + */ @Data @Builder @NoArgsConstructor diff --git a/backend/src/main/java/com/assettrack/common/exception/dto/ValidationErrorResponse.java b/backend/src/main/java/com/assettrack/common/exception/dto/ValidationErrorResponse.java index c68670b..3174250 100644 --- a/backend/src/main/java/com/assettrack/common/exception/dto/ValidationErrorResponse.java +++ b/backend/src/main/java/com/assettrack/common/exception/dto/ValidationErrorResponse.java @@ -7,6 +7,9 @@ import java.util.List; +/** + * Legacy validation error response envelope. + */ @Data @Builder @NoArgsConstructor diff --git a/backend/src/main/java/com/assettrack/config/OpenApiConfig.java b/backend/src/main/java/com/assettrack/config/OpenApiConfig.java index b0d63d3..23ab143 100644 --- a/backend/src/main/java/com/assettrack/config/OpenApiConfig.java +++ b/backend/src/main/java/com/assettrack/config/OpenApiConfig.java @@ -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"))); + } } diff --git a/backend/src/main/java/com/assettrack/config/WebConfig.java b/backend/src/main/java/com/assettrack/config/WebConfig.java index 81bff10..502c5d4 100644 --- a/backend/src/main/java/com/assettrack/config/WebConfig.java +++ b/backend/src/main/java/com/assettrack/config/WebConfig.java @@ -7,6 +7,9 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +/** + * Web MVC configuration for request-level validation and interceptors. + */ @Configuration public class WebConfig implements WebMvcConfigurer { @@ -14,7 +17,8 @@ public class WebConfig implements WebMvcConfigurer { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new HandlerInterceptor() { @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { String pageParam = request.getParameter("page"); if (pageParam != null) { try { diff --git a/backend/src/main/java/com/assettrack/controller/CustomErrorController.java b/backend/src/main/java/com/assettrack/controller/CustomErrorController.java index 8fd5235..893c024 100644 --- a/backend/src/main/java/com/assettrack/controller/CustomErrorController.java +++ b/backend/src/main/java/com/assettrack/controller/CustomErrorController.java @@ -10,6 +10,9 @@ import java.time.Instant; +/** + * Fallback controller that renders a structured ApiError response. + */ @Controller public class CustomErrorController implements ErrorController { @@ -33,10 +36,10 @@ public ResponseEntity handleError(HttpServletRequest request) { httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; } - String message = (exceptionMessage != null && !exceptionMessage.toString().isEmpty()) - ? exceptionMessage.toString() + String message = (exceptionMessage != null && !exceptionMessage.toString().isEmpty()) + ? exceptionMessage.toString() : httpStatus.getReasonPhrase(); - + String path = requestUri != null ? requestUri.toString() : request.getRequestURI(); ApiError apiError = ApiError.builder() diff --git a/backend/src/main/java/com/assettrack/controller/allocation/AllocationController.java b/backend/src/main/java/com/assettrack/controller/allocation/AllocationController.java index 6e9b7f5..d2505e7 100644 --- a/backend/src/main/java/com/assettrack/controller/allocation/AllocationController.java +++ b/backend/src/main/java/com/assettrack/controller/allocation/AllocationController.java @@ -7,7 +7,6 @@ import com.assettrack.dto.common.PagedResponse; import com.assettrack.service.allocation.IAllocationService; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -20,6 +19,16 @@ import java.util.UUID; +/** + * Asset Allocation Controller for AssetTrack. + * + * Manages asset allocation operations including assigning assets to users and + * tracking allocation history. + * Provides functionality for asset checkout, return, and allocation history + * retrieval. + * + * Base URL: {@code /api/v1/assets/{assetId}/allocations} + */ @RequestMapping("/assets/{assetId}/allocations") @RequiredArgsConstructor @RestController diff --git a/backend/src/main/java/com/assettrack/controller/asset/AssetActionController.java b/backend/src/main/java/com/assettrack/controller/asset/AssetActionController.java index a4b514b..a2ba528 100644 --- a/backend/src/main/java/com/assettrack/controller/asset/AssetActionController.java +++ b/backend/src/main/java/com/assettrack/controller/asset/AssetActionController.java @@ -20,6 +20,16 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +/** + * Asset Search Controller for AssetTrack. + * + * Provides advanced search capabilities for assets with multi-field filtering + * and spare asset lookup. + * Note: Functionality overlaps with {@link AssetController} - consider + * consolidation. + * + * Base URL: {@code /api/v1/search/assets} + */ @RestController @RequestMapping("/search/assets") @RequiredArgsConstructor @@ -35,17 +45,24 @@ public class AssetActionController { @PreAuthorize("isAuthenticated()") @Operation(summary = "Search assets", description = "Dynamic asset search with optional filters for status, type, brand, and serial number. All parameters are optional and composable.") @ApiResponse(responseCode = "200", description = "Search results returned") + /** + * Searches assets using optional filters. + * + * @param status optional status filter + * @param type optional type filter + * @param brand optional brand filter + * @param serialNumber optional serial number filter + * @param pageable pagination parameters + * @return paged asset response + */ public ResponseEntity> searchAssets( - @Parameter(description = "Filter by status (AVAILABLE, ALLOCATED, UNDER_REPAIR, DECOMMISSIONED, SPARE, EXPIRED)") - @RequestParam(required = false) AssetStatus status, - @Parameter(description = "Filter by type (LAPTOP, MONITOR, KEYBOARD, MOUSE, HEADSET, DOCKING_STATION, OTHER)") - @RequestParam(required = false) AssetType type, - @Parameter(description = "Filter by brand (case-insensitive partial match)") - @RequestParam(required = false) String brand, - @Parameter(description = "Filter by exact serial number") - @RequestParam(required = false) String serialNumber, + @Parameter(description = "Filter by status (AVAILABLE, ALLOCATED, UNDER_REPAIR, DECOMMISSIONED, SPARE, EXPIRED)") @RequestParam(required = false) AssetStatus status, + @Parameter(description = "Filter by type (LAPTOP, MONITOR, KEYBOARD, MOUSE, HEADSET, DOCKING_STATION, OTHER)") @RequestParam(required = false) AssetType type, + @Parameter(description = "Filter by brand (case-insensitive partial match)") @RequestParam(required = false) String brand, + @Parameter(description = "Filter by exact serial number") @RequestParam(required = false) String serialNumber, Pageable pageable) { - return ResponseEntity.ok(PageUtils.toPagedResponse(assetService.searchAssets(status, type, brand, serialNumber, pageable))); + return ResponseEntity + .ok(PageUtils.toPagedResponse(assetService.searchAssets(status, type, brand, serialNumber, pageable))); } @GetMapping("/spare-laptop") @@ -53,6 +70,11 @@ public ResponseEntity> searchAssets( @Operation(summary = "Get a quick spare laptop", description = "Returns the oldest available laptop asset for quick allocation") @ApiResponse(responseCode = "200", description = "Spare asset found", content = @Content(schema = @Schema(implementation = SpareAssetResponse.class))) @ApiResponse(responseCode = "404", description = "No available spare asset") + /** + * Returns the quickest available spare laptop. + * + * @return spare asset response + */ public ResponseEntity getQuickSpareLaptop() { return ResponseEntity.ok(dashboardService.getQuickSpareLaptop()); } diff --git a/backend/src/main/java/com/assettrack/controller/asset/AssetController.java b/backend/src/main/java/com/assettrack/controller/asset/AssetController.java index f27fffd..fa52895 100644 --- a/backend/src/main/java/com/assettrack/controller/asset/AssetController.java +++ b/backend/src/main/java/com/assettrack/controller/asset/AssetController.java @@ -20,6 +20,17 @@ import java.net.URI; import java.util.UUID; +import jakarta.validation.constraints.Size; + +/** + * Asset Management Controller for AssetTrack. + * + * Provides endpoints for asset CRUD operations including creation, retrieval, + * updating, and deletion. + * Supports searching and filtering assets by various criteria. + * + * Base URL: {@code /api/v1/assets} + */ @RequestMapping("/assets") @RequiredArgsConstructor @RestController @@ -30,17 +41,34 @@ public class AssetController { @GetMapping("") @PreAuthorize("isAuthenticated()") + /** + * Returns the asset list. + * + * @param status optional status filter + * @param type optional type filter + * @param brand optional brand filter + * @param serialNumber optional serial number filter + * @param pageable pagination parameters + * @return paged asset response + */ public ResponseEntity> getAssets( @Parameter(description = "Filter by status (AVAILABLE, ALLOCATED, UNDER_REPAIR, DECOMMISSIONED, SPARE, EXPIRED)") @RequestParam(required = false) AssetStatus status, @Parameter(description = "Filter by type (LAPTOP, MONITOR, KEYBOARD, MOUSE, HEADSET, DOCKING_STATION, OTHER)") @RequestParam(required = false) AssetType type, - @Parameter(description = "Filter by brand (case-insensitive partial match)") @RequestParam(required = false) String brand, - @Parameter(description = "Filter by exact serial number") @RequestParam(required = false) String serialNumber, + @Parameter(description = "Filter by brand (case-insensitive partial match)") @RequestParam(required = false) @Size(min = 1, max = 100, message = "Brand filter must be between 1 and 100 characters") String brand, + @Parameter(description = "Filter by exact serial number") @RequestParam(required = false) @Size(min = 1, max = 30, message = "Serial number filter must be between 1 and 30 characters") String serialNumber, Pageable pageable) { - return ResponseEntity.ok(PageUtils.toPagedResponse(assetService.searchAssets(status, type, brand, serialNumber, pageable))); + return ResponseEntity + .ok(PageUtils.toPagedResponse(assetService.searchAssets(status, type, brand, serialNumber, pageable))); } @PostMapping("") @PreAuthorize("hasAnyRole('ADMIN','MANAGER')") + /** + * Creates a new asset. + * + * @param request create asset payload + * @return created asset response + */ public ResponseEntity createAsset(@RequestBody @Validated CreateAssetRequest request) { AssetResponse response = assetService.registerAsset(request); URI location = URI.create("/api/v1/assets/" + response.getId()); @@ -49,12 +77,25 @@ public ResponseEntity createAsset(@RequestBody @Validated CreateA @GetMapping("/{id}") @PreAuthorize("isAuthenticated()") + /** + * Returns an asset by ID. + * + * @param id asset identifier + * @return asset response + */ public ResponseEntity getAssetById(@Parameter(description = "Asset ID") @PathVariable UUID id) { return ResponseEntity.ok(assetService.getAssetById(id)); } @PatchMapping("/{id}") @PreAuthorize("hasAnyRole('ADMIN','MANAGER')") + /** + * Updates an asset. + * + * @param id asset identifier + * @param request update payload + * @return updated asset response + */ public ResponseEntity updateAsset(@Parameter(description = "Asset ID") @PathVariable UUID id, @RequestBody @Validated UpdateAssetRequest request) { return ResponseEntity.ok(assetService.updateAsset(id, request)); @@ -62,6 +103,12 @@ public ResponseEntity updateAsset(@Parameter(description = "Asset @DeleteMapping("/{id}") @PreAuthorize("hasAnyRole('ADMIN','MANAGER')") + /** + * Deletes an asset. + * + * @param id asset identifier + * @return empty response on success + */ public ResponseEntity deleteAsset(@Parameter(description = "Asset ID") @PathVariable UUID id) { assetService.deleteAsset(id); return ResponseEntity.noContent().build(); diff --git a/backend/src/main/java/com/assettrack/controller/asset/ConditionController.java b/backend/src/main/java/com/assettrack/controller/asset/ConditionController.java index 377b3bd..7e2173f 100644 --- a/backend/src/main/java/com/assettrack/controller/asset/ConditionController.java +++ b/backend/src/main/java/com/assettrack/controller/asset/ConditionController.java @@ -23,6 +23,16 @@ import java.util.UUID; +/** + * Asset Condition Report Controller for AssetTrack. + * + * Manages asset condition reporting functionality including creation, + * retrieval, and resolution of condition reports. + * Allows users to report asset conditions and administrators to track asset + * health status. + * + * Base URL: {@code /api/v1/assets} + */ @RestController @RequestMapping("/assets") @RequiredArgsConstructor @@ -38,6 +48,14 @@ public class ConditionController { @ApiResponse(responseCode = "403", description = "Forbidden when a regular user does not own the asset") @ApiResponse(responseCode = "404", description = "Asset not found") @ApiResponse(responseCode = "422", description = "Validation error") + /** + * Creates a condition report for an asset. + * + * @param assetId asset identifier + * @param request condition report payload + * @param authentication current authentication context + * @return created condition report response + */ public ResponseEntity reportAssetCondition( @Parameter(description = "Asset ID") @PathVariable UUID assetId, @RequestBody @Validated ReportConditionRequest request, @@ -57,6 +75,13 @@ public ResponseEntity reportAssetCondition( @ApiResponse(responseCode = "403", description = "Forbidden when a regular user does not own the asset") @ApiResponse(responseCode = "404", description = "Asset not found") @ApiResponse(responseCode = "422", description = "Validation error") + /** + * Creates a legacy condition report. + * + * @param request legacy condition report payload + * @param authentication current authentication context + * @return created condition report response + */ public ResponseEntity createConditionReport( @RequestBody @Validated CreateConditionReportRequest request, Authentication authentication) { @@ -68,7 +93,15 @@ public ResponseEntity createConditionReport( @PreAuthorize("isAuthenticated()") @Operation(summary = "List condition reports", description = "Managers and admins see all reports. Regular users see only reports they submitted.") @ApiResponse(responseCode = "200", description = "Reports retrieved") - public ResponseEntity> getConditionReports(Authentication authentication, Pageable pageable) { + /** + * Lists condition reports visible to the current user. + * + * @param authentication current authentication context + * @param pageable pagination parameters + * @return paged condition report response + */ + public ResponseEntity> getConditionReports(Authentication authentication, + Pageable pageable) { return ResponseEntity.ok(PageUtils.toPagedResponse(assetService.getConditionReports(authentication, pageable))); } @@ -77,10 +110,19 @@ public ResponseEntity> getConditionReport @Operation(summary = "List condition reports for an asset", description = "Managers and admins see all reports for the asset. Regular users see only reports they submitted.") @ApiResponse(responseCode = "200", description = "Reports retrieved") @ApiResponse(responseCode = "404", description = "Asset not found") + /** + * Lists condition reports for a specific asset. + * + * @param assetId asset identifier + * @param authentication current authentication context + * @param pageable pagination parameters + * @return paged condition report response + */ public ResponseEntity> getReportsByAsset( @Parameter(description = "Asset ID") @PathVariable UUID assetId, Authentication authentication, Pageable pageable) { - return ResponseEntity.ok(PageUtils.toPagedResponse(assetService.getReportsByAsset(assetId, authentication, pageable))); + return ResponseEntity + .ok(PageUtils.toPagedResponse(assetService.getReportsByAsset(assetId, authentication, pageable))); } @GetMapping("/condition-reports/{reportId}") @@ -89,6 +131,13 @@ public ResponseEntity> getReportsByAsset( @ApiResponse(responseCode = "200", description = "Report found", content = @Content(schema = @Schema(implementation = ConditionReportResponse.class))) @ApiResponse(responseCode = "403", description = "Forbidden when a regular user does not own the report") @ApiResponse(responseCode = "404", description = "Report not found") + /** + * Returns a condition report by ID. + * + * @param reportId report identifier + * @param authentication current authentication context + * @return condition report response + */ public ResponseEntity getConditionReport( @Parameter(description = "Condition Report ID") @PathVariable UUID reportId, Authentication authentication) { @@ -101,6 +150,12 @@ public ResponseEntity getConditionReport( @ApiResponse(responseCode = "200", description = "Report resolved", content = @Content(schema = @Schema(implementation = ConditionReportResponse.class))) @ApiResponse(responseCode = "404", description = "Report not found") @ApiResponse(responseCode = "403", description = "Forbidden when Admin or Manager role is missing") + /** + * Resolves a condition report. + * + * @param reportId report identifier + * @return updated condition report response + */ public ResponseEntity resolveReport( @Parameter(description = "Condition Report ID") @PathVariable UUID reportId) { return ResponseEntity.ok(assetService.resolveReport(reportId)); diff --git a/backend/src/main/java/com/assettrack/controller/auth/AuthController.java b/backend/src/main/java/com/assettrack/controller/auth/AuthController.java index 18aa559..14269db 100644 --- a/backend/src/main/java/com/assettrack/controller/auth/AuthController.java +++ b/backend/src/main/java/com/assettrack/controller/auth/AuthController.java @@ -17,6 +17,15 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +/** + * Authentication Controller for AssetTrack. + * + * Handles public authentication operations including user registration and + * login. + * Provides JWT tokens for authenticated requests. + * + * Base URL: {@code /api/v1/auth} + */ @RestController @RequiredArgsConstructor @RequestMapping("/auth") @@ -24,21 +33,55 @@ public class AuthController { private final IAuthService authService; + /** + * Registers a new user account. + * + * Creates a new user with the provided email, password, and full name. + * Email must be unique in the system. + * + * @param request the signup request containing email, password, and full name + * @return ResponseEntity with the created user profile (HTTP 201) + * @throws ConflictException if email already exists + * @throws BadRequestException if validation fails + */ @PostMapping("/signup") @Operation(summary = "Register a new user", description = "Creates a new user account") - @ApiResponse(responseCode = "201", description = "User registered successfully", - content = @Content(schema = @Schema(implementation = com.assettrack.dto.user.UserResponse.class))) + @ApiResponse(responseCode = "201", description = "User registered successfully", content = @Content(schema = @Schema(implementation = com.assettrack.dto.user.UserResponse.class))) @ApiResponse(responseCode = "422", description = "Validation error") @ApiResponse(responseCode = "409", description = "Email already exists") - public ResponseEntity register(@RequestBody @Validated SignupRequest request) { + /** + * Registers a new user account. + * + * @param request signup payload containing email, password, and full name + * @return the created user profile + */ + public ResponseEntity register( + @RequestBody @Validated SignupRequest request) { com.assettrack.dto.user.UserResponse response = authService.register(request); return ResponseEntity.status(201).body(response); } + /** + * Authenticates a user and returns a JWT token. + * + * Validates the provided credentials and issues a JWT token on successful + * authentication. + * + * @param request the login request containing email and password + * @return ResponseEntity with authentication response including JWT token (HTTP + * 200) + * @throws UnauthorizedException if credentials are invalid + */ @PostMapping("/login") @Operation(summary = "Login", description = "Authenticates a user and returns a JWT token") @ApiResponse(responseCode = "200", description = "Login successful", content = @Content(schema = @Schema(implementation = AuthResponse.class))) @ApiResponse(responseCode = "401", description = "Invalid credentials") + /** + * Authenticates a user and returns a JWT token. + * + * @param request login payload containing email and password + * @return authentication response with the access token + */ public ResponseEntity login(@RequestBody @Validated LoginRequest request) { AuthResponse response = authService.login(request); return ResponseEntity.ok(response); diff --git a/backend/src/main/java/com/assettrack/controller/dashboard/DashboardController.java b/backend/src/main/java/com/assettrack/controller/dashboard/DashboardController.java index 77b047a..bb5805c 100644 --- a/backend/src/main/java/com/assettrack/controller/dashboard/DashboardController.java +++ b/backend/src/main/java/com/assettrack/controller/dashboard/DashboardController.java @@ -14,6 +14,15 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +/** + * Dashboard Controller for AssetTrack. + * + * Provides analytics and summary data for admin dashboard including asset + * inventories + * and status breakdowns. + * + * Base URL: {@code /api/v1/dashboard} + */ @RestController @RequestMapping("/dashboard") @RequiredArgsConstructor @@ -26,6 +35,11 @@ public class DashboardController { @PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')") @Operation(summary = "Get dashboard summary", description = "Returns asset counts by status and type for the admin dashboard") @ApiResponse(responseCode = "200", description = "Dashboard summary retrieved", content = @Content(schema = @Schema(implementation = DashboardSummaryDto.class))) + /** + * Returns the dashboard summary. + * + * @return dashboard summary response + */ public ResponseEntity getSummary() { return ResponseEntity.ok(dashboardService.getSummary()); } diff --git a/backend/src/main/java/com/assettrack/controller/notification/NotificationController.java b/backend/src/main/java/com/assettrack/controller/notification/NotificationController.java index e5baf03..9f8b88b 100644 --- a/backend/src/main/java/com/assettrack/controller/notification/NotificationController.java +++ b/backend/src/main/java/com/assettrack/controller/notification/NotificationController.java @@ -18,6 +18,14 @@ import java.util.UUID; +/** + * Notification Controller for AssetTrack. + * + * Manages in-app user notifications including retrieval, marking as read, + * and notification preferences management. + * + * Base URL: {@code /api/v1/notifications} + */ @RestController @RequestMapping("/notifications") @RequiredArgsConstructor @@ -30,8 +38,17 @@ public class NotificationController { @PreAuthorize("isAuthenticated()") @Operation(summary = "List my notifications", description = "Returns paginated in-app alerts for the authenticated user.") @ApiResponse(responseCode = "200", description = "Notifications retrieved") - public ResponseEntity> getNotifications(Authentication authentication, Pageable pageable) { - return ResponseEntity.ok(PageUtils.toPagedResponse(notificationService.getCurrentUserNotifications(authentication, pageable))); + /** + * Returns the current user's notifications. + * + * @param authentication current authentication context + * @param pageable pagination parameters + * @return paged notification response + */ + public ResponseEntity> getNotifications(Authentication authentication, + Pageable pageable) { + return ResponseEntity.ok( + PageUtils.toPagedResponse(notificationService.getCurrentUserNotifications(authentication, pageable))); } @PatchMapping("/{notificationId}/read") @@ -39,6 +56,13 @@ public ResponseEntity> getNotifications(Auth @Operation(summary = "Mark notification as read", description = "Marks one of the authenticated user's notifications as read.") @ApiResponse(responseCode = "200", description = "Notification marked as read", content = @Content(schema = @Schema(implementation = NotificationResponse.class))) @ApiResponse(responseCode = "404", description = "Notification not found") + /** + * Marks a notification as read. + * + * @param notificationId notification identifier + * @param authentication current authentication context + * @return updated notification response + */ public ResponseEntity markAsRead( @PathVariable UUID notificationId, Authentication authentication) { diff --git a/backend/src/main/java/com/assettrack/controller/user/UserController.java b/backend/src/main/java/com/assettrack/controller/user/UserController.java index faac018..1b653a7 100644 --- a/backend/src/main/java/com/assettrack/controller/user/UserController.java +++ b/backend/src/main/java/com/assettrack/controller/user/UserController.java @@ -22,12 +22,28 @@ import java.util.UUID; +/** + * User Management Controller for AssetTrack. + * + * Handles user management operations including profile retrieval, user listing, + * email/password updates, + * and admin operations for user role and status management. + * + * Base URLs: {@code /api/v1/auth}, {@code /api/v1/users} + */ @RestController @RequiredArgsConstructor @Tag(name = "Users", description = "User management and self-service endpoints") public class UserController { private final IUserService userService; + /** + * Retrieves the authenticated user's profile. + * + * @param authentication the authenticated user + * @return ResponseEntity with the user's profile (HTTP 200) + * @throws UnauthorizedException if not authenticated + */ @GetMapping("/auth/me") @PreAuthorize("isAuthenticated()") @Operation(summary = "Get my profile", description = "Returns the authenticated user's profile") @@ -43,7 +59,13 @@ public ResponseEntity getMyProfile(Authentication authentication) @Operation(summary = "List all users", description = "Returns a paginated list of all users") @ApiResponse(responseCode = "200", description = "Users retrieved") @ApiResponse(responseCode = "403", description = "Forbidden") - public ResponseEntity> getAllUsers(Pageable pageable){ + /** + * Returns the user list. + * + * @param pageable pagination parameters + * @return paged user response + */ + public ResponseEntity> getAllUsers(Pageable pageable) { return ResponseEntity.ok(PageUtils.toPagedResponse(userService.getAllUsers(pageable))); } @@ -52,6 +74,12 @@ public ResponseEntity> getAllUsers(Pageable pageable @Operation(summary = "List inactive users", description = "Returns a paginated list of deactivated users (Admin only)") @ApiResponse(responseCode = "200", description = "Inactive users retrieved") @ApiResponse(responseCode = "403", description = "Forbidden – Admin role required") + /** + * Returns inactive users. + * + * @param pageable pagination parameters + * @return paged inactive user response + */ public ResponseEntity> getInActiveUsers(Pageable pageable) { return ResponseEntity.ok(PageUtils.toPagedResponse(userService.getInactiveUsers(pageable))); } @@ -61,6 +89,12 @@ public ResponseEntity> getInActiveUsers(Pageable pag @Operation(summary = "Get user by ID", description = "Returns a single user by their ID") @ApiResponse(responseCode = "200", description = "User found", content = @Content(schema = @Schema(implementation = UserResponse.class))) @ApiResponse(responseCode = "404", description = "User not found") + /** + * Returns a single user by ID. + * + * @param id user identifier + * @return user response + */ public ResponseEntity getUser(@Parameter(description = "User ID") @PathVariable UUID id) { return ResponseEntity.ok(userService.getUserById(id)); } @@ -70,6 +104,13 @@ public ResponseEntity getUser(@Parameter(description = "User ID") @Operation(summary = "Update my email", description = "Updates the authenticated user's email address") @ApiResponse(responseCode = "200", description = "Email updated", content = @Content(schema = @Schema(implementation = UserResponse.class))) @ApiResponse(responseCode = "409", description = "Email already taken") + /** + * Updates the authenticated user's email address. + * + * @param request new email payload + * @param authentication current authentication context + * @return updated user response + */ public ResponseEntity updateEmail(@RequestBody @Validated UpdateEmailRequest request, Authentication authentication) { return ResponseEntity.ok(userService.updateEmail(request, authentication)); @@ -80,6 +121,13 @@ public ResponseEntity updateEmail(@RequestBody @Validated UpdateEm @Operation(summary = "Update my password", description = "Changes the authenticated user's password") @ApiResponse(responseCode = "204", description = "Password updated") @ApiResponse(responseCode = "400", description = "Invalid current password") + /** + * Updates the authenticated user's password. + * + * @param request password change payload + * @param authentication current authentication context + * @return empty response on success + */ public ResponseEntity updatePassword(@RequestBody @Validated UpdatePasswordRequest request, Authentication authentication) { userService.updatePassword(request, authentication); @@ -92,6 +140,14 @@ public ResponseEntity updatePassword(@RequestBody @Validated UpdatePasswor @ApiResponse(responseCode = "200", description = "Role updated", content = @Content(schema = @Schema(implementation = UserResponse.class))) @ApiResponse(responseCode = "404", description = "User not found") @ApiResponse(responseCode = "400", description = "Invalid role") + /** + * Updates a user's role. + * + * @param id user identifier + * @param role new role name + * @param authentication current authentication context + * @return updated user response + */ public ResponseEntity updateUserRole( @Parameter(description = "User ID") @PathVariable UUID id, @Parameter(description = "New role (ADMIN, MANAGER, DEVELOPER)") @RequestParam String role, @@ -104,6 +160,14 @@ public ResponseEntity updateUserRole( @Operation(summary = "Update user status", description = "Activates or deactivates a user (Admin only)") @ApiResponse(responseCode = "200", description = "Status updated", content = @Content(schema = @Schema(implementation = UserResponse.class))) @ApiResponse(responseCode = "404", description = "User not found") + /** + * Updates a user's active status. + * + * @param id user identifier + * @param active whether the account should be active + * @param authentication current authentication context + * @return updated user response + */ public ResponseEntity updateUserStatus( @Parameter(description = "User ID") @PathVariable UUID id, @Parameter(description = "Active status") @RequestParam boolean active, @@ -115,6 +179,12 @@ public ResponseEntity updateUserStatus( @PreAuthorize("isAuthenticated()") @Operation(summary = "Delete my account", description = "Permanently deletes the authenticated user's account") @ApiResponse(responseCode = "204", description = "Account deleted") + /** + * Deletes the authenticated user's account. + * + * @param authentication current authentication context + * @return empty response on success + */ public ResponseEntity deleteAccount(Authentication authentication) { userService.deleteSelf(authentication); return ResponseEntity.noContent().build(); @@ -125,6 +195,13 @@ public ResponseEntity deleteAccount(Authentication authentication) { @Operation(summary = "Delete user", description = "Permanently deletes a user by ID (Admin only)") @ApiResponse(responseCode = "204", description = "User deleted") @ApiResponse(responseCode = "404", description = "User not found") + /** + * Deletes a user by ID. + * + * @param id user identifier + * @param authentication current authentication context + * @return empty response on success + */ public ResponseEntity deleteUser( @Parameter(description = "User ID") @PathVariable UUID id, Authentication authentication) { diff --git a/backend/src/main/java/com/assettrack/domain/asset/Asset.java b/backend/src/main/java/com/assettrack/domain/asset/Asset.java index d27c478..56e4386 100644 --- a/backend/src/main/java/com/assettrack/domain/asset/Asset.java +++ b/backend/src/main/java/com/assettrack/domain/asset/Asset.java @@ -1,6 +1,5 @@ package com.assettrack.domain.asset; - import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; @@ -13,6 +12,9 @@ import java.util.List; import java.util.UUID; +/** + * Asset entity representing a tracked hardware item. + */ @Entity @Table(name = "assets") @Data @@ -21,51 +23,60 @@ @AllArgsConstructor public class Asset { + /** Asset identifier. */ @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; + /** Asset type. */ @Enumerated(EnumType.STRING) @Column(nullable = false) private AssetType type; + /** Brand name. */ @Column(nullable = false, length = 100) private String brand; + /** Model name. */ @Column(nullable = false, length = 150) private String model; + /** Unique serial number. */ @Column(nullable = false, unique = true, length = 30) private String serialNumber; + /** Purchase date. */ private LocalDate purchaseDate; + /** Warranty expiration date. */ private LocalDate warrantyExpirationDate; + /** Current asset status. */ @Enumerated(EnumType.STRING) @Column(nullable = false) private AssetStatus status; + /** Optional internal notes. */ @Column(length = 1000) private String notes; - // One asset → many allocation records (full history) - @OneToMany(mappedBy = "asset", fetch = FetchType.LAZY, - cascade = CascadeType.ALL, orphanRemoval = true) + /** Allocation history for this asset. */ + @OneToMany(mappedBy = "asset", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) @OrderBy("checkoutDate DESC") @Builder.Default private List allocations = new ArrayList<>(); - // One asset → many condition reports - @OneToMany(mappedBy = "asset", fetch = FetchType.LAZY, - cascade = CascadeType.ALL, orphanRemoval = true) + /** Condition reports filed against this asset. */ + @OneToMany(mappedBy = "asset", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) @OrderBy("reportDate DESC") @Builder.Default private List conditionReports = new ArrayList<>(); + /** UTC creation timestamp. */ @Column(nullable = false, updatable = false) private LocalDateTime createdAt; + /** UTC last update timestamp. */ @Column(name = "updated_at") private LocalDateTime updatedAt; diff --git a/backend/src/main/java/com/assettrack/domain/asset/AssetAllocation.java b/backend/src/main/java/com/assettrack/domain/asset/AssetAllocation.java index 09efc34..9a80b1e 100644 --- a/backend/src/main/java/com/assettrack/domain/asset/AssetAllocation.java +++ b/backend/src/main/java/com/assettrack/domain/asset/AssetAllocation.java @@ -17,6 +17,9 @@ import java.time.LocalDateTime; import java.util.UUID; +/** + * Asset allocation entity representing a checkout record. + */ @Entity @Table(name = "asset_allocations") @Data @@ -25,26 +28,29 @@ @AllArgsConstructor public class AssetAllocation { + /** Allocation identifier. */ @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; - // Many allocations → one asset + /** Allocated asset. */ @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "asset_id", nullable = false) private Asset asset; - // Many allocations → one user + /** User receiving the asset. */ @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_id", nullable = false) private User user; + /** UTC checkout timestamp. */ @Column(nullable = false) private LocalDateTime checkoutDate; - // null = currently active allocation + /** UTC return timestamp; null while the allocation is active. */ private LocalDateTime returnDate; + /** Optional allocation notes. */ @Column(length = 500) private String notes; } diff --git a/backend/src/main/java/com/assettrack/domain/asset/AssetStatus.java b/backend/src/main/java/com/assettrack/domain/asset/AssetStatus.java index 76e3f11..8dfd5d6 100644 --- a/backend/src/main/java/com/assettrack/domain/asset/AssetStatus.java +++ b/backend/src/main/java/com/assettrack/domain/asset/AssetStatus.java @@ -1,10 +1,13 @@ package com.assettrack.domain.asset; +/** + * Lifecycle status for an asset. + */ public enum AssetStatus { AVAILABLE, ALLOCATED, UNDER_REPAIR, DECOMMISSIONED, SPARE, - EXPIRED + EXPIRED } diff --git a/backend/src/main/java/com/assettrack/domain/asset/AssetType.java b/backend/src/main/java/com/assettrack/domain/asset/AssetType.java index 2a38c12..704b17e 100644 --- a/backend/src/main/java/com/assettrack/domain/asset/AssetType.java +++ b/backend/src/main/java/com/assettrack/domain/asset/AssetType.java @@ -1,5 +1,8 @@ package com.assettrack.domain.asset; +/** + * Supported asset categories. + */ public enum AssetType { LAPTOP, MONITOR, diff --git a/backend/src/main/java/com/assettrack/domain/asset/ConditionReport.java b/backend/src/main/java/com/assettrack/domain/asset/ConditionReport.java index 9de1c56..6ca98b6 100644 --- a/backend/src/main/java/com/assettrack/domain/asset/ConditionReport.java +++ b/backend/src/main/java/com/assettrack/domain/asset/ConditionReport.java @@ -23,8 +23,7 @@ import java.util.UUID; /** - * Represents a condition/issue report filed against an asset. - * Tracks reported issues from creation (OPEN) through resolution (RESOLVED). + * Condition report entity representing an issue filed against an asset. */ @Entity @Table(name = "condition_reports") @@ -34,38 +33,45 @@ @AllArgsConstructor public class ConditionReport { + /** Condition report identifier. */ @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; - // Many reports → one asset + /** Asset associated with the report. */ @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "asset_id", nullable = false) private Asset asset; - // Many reports → one user (the reporter) + /** User who submitted the report. */ @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "reported_by", nullable = false) private User reportedBy; + /** Detailed issue description. */ @Column(nullable = false, columnDefinition = "TEXT") private String issueDescription; + /** UTC report timestamp. */ @Column(nullable = false) private LocalDateTime reportDate; + /** Report severity. */ @Enumerated(EnumType.STRING) @Column(nullable = false) private ConditionSeverity severity; + /** Current workflow status. */ @Enumerated(EnumType.STRING) @Column(nullable = false) @Builder.Default private ConditionReportStatus status = ConditionReportStatus.OPEN; + /** Optional resolution notes. */ @Column(columnDefinition = "TEXT") private String resolutionNotes; + /** UTC last update timestamp. */ @Column(nullable = false) @Builder.Default private LocalDateTime updatedAt = LocalDateTime.now(); diff --git a/backend/src/main/java/com/assettrack/domain/asset/ConditionReportStatus.java b/backend/src/main/java/com/assettrack/domain/asset/ConditionReportStatus.java index e20c3ac..6c43a64 100644 --- a/backend/src/main/java/com/assettrack/domain/asset/ConditionReportStatus.java +++ b/backend/src/main/java/com/assettrack/domain/asset/ConditionReportStatus.java @@ -1,5 +1,8 @@ package com.assettrack.domain.asset; +/** + * Workflow status for a condition report. + */ public enum ConditionReportStatus { OPEN, IN_PROGRESS, diff --git a/backend/src/main/java/com/assettrack/domain/asset/ConditionSeverity.java b/backend/src/main/java/com/assettrack/domain/asset/ConditionSeverity.java index f7b9901..94c7e66 100644 --- a/backend/src/main/java/com/assettrack/domain/asset/ConditionSeverity.java +++ b/backend/src/main/java/com/assettrack/domain/asset/ConditionSeverity.java @@ -1,5 +1,8 @@ package com.assettrack.domain.asset; +/** + * Severity level for a reported condition issue. + */ public enum ConditionSeverity { LOW, MEDIUM, diff --git a/backend/src/main/java/com/assettrack/domain/asset/ReportStatus.java b/backend/src/main/java/com/assettrack/domain/asset/ReportStatus.java index 99c287a..b496d64 100644 --- a/backend/src/main/java/com/assettrack/domain/asset/ReportStatus.java +++ b/backend/src/main/java/com/assettrack/domain/asset/ReportStatus.java @@ -1,5 +1,8 @@ package com.assettrack.domain.asset; +/** + * Status values used by legacy report flows. + */ public enum ReportStatus { OPEN, IN_PROGRESS, diff --git a/backend/src/main/java/com/assettrack/domain/notification/Notification.java b/backend/src/main/java/com/assettrack/domain/notification/Notification.java index b531718..3a8bc9b 100644 --- a/backend/src/main/java/com/assettrack/domain/notification/Notification.java +++ b/backend/src/main/java/com/assettrack/domain/notification/Notification.java @@ -12,6 +12,9 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; +/** + * Notification entity stored for in-app alerts. + */ @Entity @Data @Builder @@ -19,27 +22,34 @@ @AllArgsConstructor public class Notification { + /** Notification identifier. */ @Id @GeneratedValue(strategy = jakarta.persistence.GenerationType.UUID) private java.util.UUID id; + /** User identifier used as the recipient. */ @Column(nullable = false) private String recipient; + /** Message body shown to the recipient. */ @Column(nullable = false, columnDefinition = "TEXT") private String messageBody; + /** Notification type. */ @Column(nullable = false) @Enumerated(EnumType.STRING) private NotificationType type; + /** Related asset identifier, if any. */ @Column private java.util.UUID assetId; + /** Whether the notification has been read. */ @Column(nullable = false) @Builder.Default private boolean isRead = false; + /** UTC creation timestamp. */ @Column(nullable = false) @Builder.Default private LocalDateTime createdAt = LocalDateTime.now(); diff --git a/backend/src/main/java/com/assettrack/domain/notification/NotificationType.java b/backend/src/main/java/com/assettrack/domain/notification/NotificationType.java index 9d6f1bb..53f3603 100644 --- a/backend/src/main/java/com/assettrack/domain/notification/NotificationType.java +++ b/backend/src/main/java/com/assettrack/domain/notification/NotificationType.java @@ -1,5 +1,8 @@ package com.assettrack.domain.notification; +/** + * Notification categories supported by the system. + */ public enum NotificationType { WARRANTY_EXPIRY, LOW_STOCK, diff --git a/backend/src/main/java/com/assettrack/domain/user/Role.java b/backend/src/main/java/com/assettrack/domain/user/Role.java index f62b001..a5d6e48 100644 --- a/backend/src/main/java/com/assettrack/domain/user/Role.java +++ b/backend/src/main/java/com/assettrack/domain/user/Role.java @@ -1,6 +1,8 @@ package com.assettrack.domain.user; - +/** + * Application roles that control access to backend operations. + */ public enum Role { ADMIN("Admin"), diff --git a/backend/src/main/java/com/assettrack/domain/user/User.java b/backend/src/main/java/com/assettrack/domain/user/User.java index bf8c4eb..7b6885b 100644 --- a/backend/src/main/java/com/assettrack/domain/user/User.java +++ b/backend/src/main/java/com/assettrack/domain/user/User.java @@ -1,4 +1,5 @@ package com.assettrack.domain.user; + import com.assettrack.domain.asset.AssetAllocation; import com.assettrack.domain.asset.ConditionReport; import jakarta.persistence.*; @@ -8,6 +9,9 @@ import java.util.List; import java.util.UUID; +/** + * User entity representing an application account. + */ @Entity @Table(name = "users") @Getter @@ -17,44 +21,53 @@ @Builder public class User { + /** User identifier. */ @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; + /** Unique email address used for authentication. */ @Column(nullable = false, unique = true) private String email; + /** BCrypt password hash. */ @Column(nullable = false) private String passwordHash; + /** Given name. */ @Column(name = "first_name") private String firstName; + /** Family name. */ @Column(name = "last_name") private String lastName; + /** Assigned application role. */ @Enumerated(EnumType.STRING) @Column(nullable = false) private Role role; + /** Whether the account is active. */ @Column(nullable = false) @Builder.Default private boolean isActive = true; - // One user → many allocations (their full history) + /** Full allocation history for the user. */ @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) @OrderBy("checkoutDate DESC") @Builder.Default private List allocations = new ArrayList<>(); - // One user → many reports they filed + /** Condition reports submitted by the user. */ @OneToMany(mappedBy = "reportedBy", fetch = FetchType.LAZY) @Builder.Default private List conditionReports = new ArrayList<>(); + /** UTC creation timestamp. */ @Column(nullable = false, updatable = false) private LocalDateTime createdAt; + /** UTC last update timestamp. */ @Column(name = "updated_at") private LocalDateTime updatedAt; diff --git a/backend/src/main/java/com/assettrack/dto/allocation/AllocationHistoryDto.java b/backend/src/main/java/com/assettrack/dto/allocation/AllocationHistoryDto.java index 7c398d0..799a3c2 100644 --- a/backend/src/main/java/com/assettrack/dto/allocation/AllocationHistoryDto.java +++ b/backend/src/main/java/com/assettrack/dto/allocation/AllocationHistoryDto.java @@ -10,28 +10,40 @@ import java.time.LocalDateTime; import com.fasterxml.jackson.annotation.JsonFormat; +/** + * Historical allocation entry used for asset allocation timelines. + */ @Getter @NoArgsConstructor @AllArgsConstructor @Builder public class AllocationHistoryDto { + /** Allocation identifier. */ private java.util.UUID allocationId; + /** Related asset identifier. */ private java.util.UUID assetId; + /** Asset serial number. */ private String assetSerialNumber; + /** Asset brand. */ private String assetBrand; + /** Asset model. */ private String assetModel; + /** User assigned to the asset. */ private UserSummary assignedTo; + /** Allocation start timestamp. */ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") private LocalDateTime allocatedAt; + /** Allocation end timestamp, if returned. */ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") private LocalDateTime deallocatedAt; + /** Number of whole days the allocation lasted. */ private Integer durationDays; } diff --git a/backend/src/main/java/com/assettrack/dto/allocation/AllocationRequestDto.java b/backend/src/main/java/com/assettrack/dto/allocation/AllocationRequestDto.java index 17e59ef..81374d2 100644 --- a/backend/src/main/java/com/assettrack/dto/allocation/AllocationRequestDto.java +++ b/backend/src/main/java/com/assettrack/dto/allocation/AllocationRequestDto.java @@ -6,6 +6,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; +/** + * Request payload for allocating an asset to a user. + */ @Getter @NoArgsConstructor @AllArgsConstructor diff --git a/backend/src/main/java/com/assettrack/dto/allocation/AllocationResponseDto.java b/backend/src/main/java/com/assettrack/dto/allocation/AllocationResponseDto.java index 36537e7..71b74cc 100644 --- a/backend/src/main/java/com/assettrack/dto/allocation/AllocationResponseDto.java +++ b/backend/src/main/java/com/assettrack/dto/allocation/AllocationResponseDto.java @@ -8,25 +8,35 @@ import java.time.LocalDateTime; import com.fasterxml.jackson.annotation.JsonFormat; +/** + * Allocation response returned after allocation and deallocation operations. + */ @Getter @NoArgsConstructor @AllArgsConstructor @Builder public class AllocationResponseDto { + /** Allocation identifier. */ private java.util.UUID id; + /** Related asset identifier. */ private java.util.UUID assetId; + /** User assigned to the asset. */ private UserSummary assignedTo; + /** Allocation start timestamp. */ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") private LocalDateTime allocatedAt; + /** Allocation end timestamp, if returned. */ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") private LocalDateTime deallocatedAt; + /** Optional notes attached to the allocation. */ private String notes; + /** Whether the allocation is currently active. */ private boolean active; } diff --git a/backend/src/main/java/com/assettrack/dto/asset/AssetResponse.java b/backend/src/main/java/com/assettrack/dto/asset/AssetResponse.java index 1930599..7560578 100644 --- a/backend/src/main/java/com/assettrack/dto/asset/AssetResponse.java +++ b/backend/src/main/java/com/assettrack/dto/asset/AssetResponse.java @@ -12,6 +12,9 @@ import java.time.LocalDateTime; import com.fasterxml.jackson.annotation.JsonFormat; +/** + * Full asset response returned by asset endpoints. + */ @Data @Builder @NoArgsConstructor diff --git a/backend/src/main/java/com/assettrack/dto/asset/AssetSummaryResponse.java b/backend/src/main/java/com/assettrack/dto/asset/AssetSummaryResponse.java index a827476..3be55df 100644 --- a/backend/src/main/java/com/assettrack/dto/asset/AssetSummaryResponse.java +++ b/backend/src/main/java/com/assettrack/dto/asset/AssetSummaryResponse.java @@ -8,14 +8,22 @@ import java.util.UUID; +/** + * Compact asset representation used inside nested responses. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class AssetSummaryResponse { + /** Asset identifier. */ private UUID id; + /** Asset type. */ private AssetType type; + /** Brand name. */ private String brand; + /** Model name. */ private String model; + /** Serial number. */ private String serialNumber; } diff --git a/backend/src/main/java/com/assettrack/dto/asset/ConditionReportResponse.java b/backend/src/main/java/com/assettrack/dto/asset/ConditionReportResponse.java index 65df4f0..012a0fc 100644 --- a/backend/src/main/java/com/assettrack/dto/asset/ConditionReportResponse.java +++ b/backend/src/main/java/com/assettrack/dto/asset/ConditionReportResponse.java @@ -12,23 +12,36 @@ import java.time.LocalDateTime; import java.util.UUID; +/** + * Response returned for condition report operations. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class ConditionReportResponse { + /** Condition report identifier. */ private UUID id; + /** Related asset identifier. */ private UUID assetId; + /** Compact asset details. */ private AssetSummaryResponse asset; + /** User who submitted the report. */ private UserSummary reportedBy; + /** Issue description. */ private String description; + /** Severity of the issue. */ private ConditionSeverity severity; + /** Current report status. */ private ConditionReportStatus status; + /** Optional resolution notes. */ private String resolutionNotes; - + + /** UTC timestamp when the report was created. */ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") private LocalDateTime reportedAt; - + + /** UTC timestamp when the report was last updated. */ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") private LocalDateTime updatedAt; } diff --git a/backend/src/main/java/com/assettrack/dto/asset/CreateAssetRequest.java b/backend/src/main/java/com/assettrack/dto/asset/CreateAssetRequest.java index 97b7565..f10697a 100644 --- a/backend/src/main/java/com/assettrack/dto/asset/CreateAssetRequest.java +++ b/backend/src/main/java/com/assettrack/dto/asset/CreateAssetRequest.java @@ -9,44 +9,52 @@ import java.time.LocalDate; +/** + * Request payload used to register a new asset. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CreateAssetRequest { + /** Asset type. */ @NotNull(message = "Asset type is required") private AssetType type; + /** Manufacturer or brand name. */ @NotBlank(message = "Brand is required") @Size(min = 1, max = 100, message = "Brand must be between 1 and 100 characters") private String brand; + /** Product model name. */ @NotBlank(message = "Model is required") @Size(min = 1, max = 150, message = "Model must be between 1 and 150 characters") private String model; + /** Unique serial number. */ @NotBlank(message = "Serial number is required") - @Pattern( - regexp = "^[A-Z0-9\\-]{4,30}$", - message = "Serial number must be 4–30 uppercase alphanumeric characters or hyphens" - ) + @Pattern(regexp = "^[A-Z0-9\\-]{4,30}$", message = "Serial number must be 4–30 uppercase alphanumeric characters or hyphens") private String serialNumber; + /** Date the asset was purchased. */ @NotNull(message = "Purchase date is required") @PastOrPresent(message = "Purchase date cannot be in the future") private LocalDate purchaseDate; + /** Warranty expiration date. */ @NotNull(message = "Warranty expiration date is required") @Future(message = "Warranty expiration date must be in the future") private LocalDate warrantyExpirationDate; + /** Optional internal notes. */ @Size(max = 1000, message = "Notes must not exceed 1000 characters") private String notes; @AssertTrue(message = "Warranty expiration date must be after purchase date") private boolean isWarrantyAfterPurchase() { - if (purchaseDate == null || warrantyExpirationDate == null) return true; + if (purchaseDate == null || warrantyExpirationDate == null) + return true; return warrantyExpirationDate.isAfter(purchaseDate); } } diff --git a/backend/src/main/java/com/assettrack/dto/asset/CreateConditionReportRequest.java b/backend/src/main/java/com/assettrack/dto/asset/CreateConditionReportRequest.java index 8fd90cd..c48df58 100644 --- a/backend/src/main/java/com/assettrack/dto/asset/CreateConditionReportRequest.java +++ b/backend/src/main/java/com/assettrack/dto/asset/CreateConditionReportRequest.java @@ -11,6 +11,9 @@ import java.util.UUID; +/** + * Request payload for creating a condition report using an asset ID. + */ @Data @Builder @NoArgsConstructor diff --git a/backend/src/main/java/com/assettrack/dto/asset/ReportConditionRequest.java b/backend/src/main/java/com/assettrack/dto/asset/ReportConditionRequest.java index 67b8748..9abc6d6 100644 --- a/backend/src/main/java/com/assettrack/dto/asset/ReportConditionRequest.java +++ b/backend/src/main/java/com/assettrack/dto/asset/ReportConditionRequest.java @@ -8,6 +8,9 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Request payload for reporting an asset condition through the asset route. + */ @Data @Builder @NoArgsConstructor diff --git a/backend/src/main/java/com/assettrack/dto/asset/Severity.java b/backend/src/main/java/com/assettrack/dto/asset/Severity.java index 82a5924..2abf4fd 100644 --- a/backend/src/main/java/com/assettrack/dto/asset/Severity.java +++ b/backend/src/main/java/com/assettrack/dto/asset/Severity.java @@ -1,7 +1,11 @@ package com.assettrack.dto.asset; +/** + * Legacy severity values used by asset condition request models. + */ public enum Severity { LOW, MEDIUM, HIGH, - CRITICAL } + CRITICAL +} diff --git a/backend/src/main/java/com/assettrack/dto/asset/SpareAssetResponse.java b/backend/src/main/java/com/assettrack/dto/asset/SpareAssetResponse.java index edccc69..7146ebe 100644 --- a/backend/src/main/java/com/assettrack/dto/asset/SpareAssetResponse.java +++ b/backend/src/main/java/com/assettrack/dto/asset/SpareAssetResponse.java @@ -8,12 +8,18 @@ import java.time.LocalDateTime; +/** + * Response returned when looking up an available spare laptop. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class SpareAssetResponse { + /** Spare asset details. */ private AssetResponse asset; + /** Most recent owner before deallocation. */ private UserSummary lastOwner; + /** When the asset was last returned. */ private LocalDateTime lastDeallocatedAt; } \ No newline at end of file diff --git a/backend/src/main/java/com/assettrack/dto/asset/UpdateAssetRequest.java b/backend/src/main/java/com/assettrack/dto/asset/UpdateAssetRequest.java index 8112e45..67f025e 100644 --- a/backend/src/main/java/com/assettrack/dto/asset/UpdateAssetRequest.java +++ b/backend/src/main/java/com/assettrack/dto/asset/UpdateAssetRequest.java @@ -13,6 +13,9 @@ import java.time.LocalDate; +/** + * Request payload used to update an asset. + */ @Data @Builder @NoArgsConstructor @@ -21,31 +24,36 @@ public class UpdateAssetRequest { // type is intentionally excluded — type cannot change after registration + /** Updated brand name. */ @Size(min = 1, max = 100, message = "Brand must be between 1 and 100 characters") private String brand; + /** Updated model name. */ @Size(min = 1, max = 150, message = "Model must be between 1 and 150 characters") private String model; - @Pattern( - regexp = "^[A-Z0-9\\-]{4,30}$", - message = "Serial number must be 4–30 uppercase alphanumeric characters or hyphens" - ) + /** Updated serial number. */ + @Pattern(regexp = "^[A-Z0-9\\-]{4,30}$", message = "Serial number must be 4–30 uppercase alphanumeric characters or hyphens") private String serialNumber; + /** Updated purchase date. */ @PastOrPresent(message = "Purchase date cannot be in the future") private LocalDate purchaseDate; + /** Updated warranty expiration date. */ private LocalDate warrantyExpirationDate; + /** Updated asset status. */ private AssetStatus status; + /** Updated internal notes. */ @Size(max = 1000, message = "Notes must not exceed 1000 characters") private String notes; @AssertTrue(message = "Warranty expiration date must be after purchase date") private boolean isWarrantyAfterPurchase() { - if (purchaseDate == null || warrantyExpirationDate == null) return true; + if (purchaseDate == null || warrantyExpirationDate == null) + return true; return warrantyExpirationDate.isAfter(purchaseDate); } } \ No newline at end of file diff --git a/backend/src/main/java/com/assettrack/dto/auth/AuthResponse.java b/backend/src/main/java/com/assettrack/dto/auth/AuthResponse.java index 3778b18..e5d20a5 100644 --- a/backend/src/main/java/com/assettrack/dto/auth/AuthResponse.java +++ b/backend/src/main/java/com/assettrack/dto/auth/AuthResponse.java @@ -5,12 +5,19 @@ import com.assettrack.dto.user.UserSummary; +/** + * Authentication response returned after login. + */ @Data @AllArgsConstructor public class AuthResponse { + /** Signed JWT access token. */ private String accessToken; + /** Token type, typically Bearer. */ private String tokenType; + /** Token lifetime in seconds. */ private long expiresIn; + /** Authenticated user summary. */ private UserSummary user; } \ No newline at end of file diff --git a/backend/src/main/java/com/assettrack/dto/auth/LoginRequest.java b/backend/src/main/java/com/assettrack/dto/auth/LoginRequest.java index 641bde2..740b910 100644 --- a/backend/src/main/java/com/assettrack/dto/auth/LoginRequest.java +++ b/backend/src/main/java/com/assettrack/dto/auth/LoginRequest.java @@ -4,12 +4,17 @@ import jakarta.validation.constraints.NotBlank; import lombok.Data; +/** + * Login payload used to authenticate an existing user. + */ @Data public class LoginRequest { + /** User email address. */ @NotBlank(message = "Email is required") @Email(message = "Email must be valid") private String email; + /** Raw password used for authentication. */ @NotBlank(message = "Password is required") private String password; } \ No newline at end of file diff --git a/backend/src/main/java/com/assettrack/dto/auth/SignupRequest.java b/backend/src/main/java/com/assettrack/dto/auth/SignupRequest.java index ed2c9a8..1c393e6 100644 --- a/backend/src/main/java/com/assettrack/dto/auth/SignupRequest.java +++ b/backend/src/main/java/com/assettrack/dto/auth/SignupRequest.java @@ -5,17 +5,23 @@ import jakarta.validation.constraints.Size; import lombok.Data; +/** + * Sign-up payload for creating a new account. + */ @Data public class SignupRequest { + /** User email address. */ @NotBlank(message = "Email is required") @Email(message = "Email must be valid") private String email; + /** Display name shown in the application. */ @NotBlank(message = "Full name is required") @Size(min = 2, max = 120, message = "Full name must be between 2 and 120 characters") private String fullName; + /** Raw password submitted during registration. */ @NotBlank(message = "Password is required") @Size(min = 8, max = 128, message = "Password must be between 8 and 128 characters") private String password; diff --git a/backend/src/main/java/com/assettrack/dto/common/PageMeta.java b/backend/src/main/java/com/assettrack/dto/common/PageMeta.java index 968ca6f..eebc80f 100644 --- a/backend/src/main/java/com/assettrack/dto/common/PageMeta.java +++ b/backend/src/main/java/com/assettrack/dto/common/PageMeta.java @@ -5,6 +5,9 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Pagination metadata included in paged responses. + */ @Data @Builder @NoArgsConstructor diff --git a/backend/src/main/java/com/assettrack/dto/common/PageUtils.java b/backend/src/main/java/com/assettrack/dto/common/PageUtils.java index 8cf6362..2df3073 100644 --- a/backend/src/main/java/com/assettrack/dto/common/PageUtils.java +++ b/backend/src/main/java/com/assettrack/dto/common/PageUtils.java @@ -2,6 +2,9 @@ import org.springframework.data.domain.Page; +/** + * Helper methods for mapping Spring Data pages to API responses. + */ public class PageUtils { public static PagedResponse toPagedResponse(Page page) { diff --git a/backend/src/main/java/com/assettrack/dto/common/PagedResponse.java b/backend/src/main/java/com/assettrack/dto/common/PagedResponse.java index 0ba909e..e731a3b 100644 --- a/backend/src/main/java/com/assettrack/dto/common/PagedResponse.java +++ b/backend/src/main/java/com/assettrack/dto/common/PagedResponse.java @@ -7,6 +7,9 @@ import java.util.List; +/** + * Generic paged response wrapper used by list endpoints. + */ @Data @Builder @NoArgsConstructor diff --git a/backend/src/main/java/com/assettrack/dto/dashboard/DashboardSummaryDto.java b/backend/src/main/java/com/assettrack/dto/dashboard/DashboardSummaryDto.java index 6a750f4..06598f0 100644 --- a/backend/src/main/java/com/assettrack/dto/dashboard/DashboardSummaryDto.java +++ b/backend/src/main/java/com/assettrack/dto/dashboard/DashboardSummaryDto.java @@ -7,15 +7,25 @@ import java.util.List; +/** + * Dashboard summary response containing inventory and health metrics. + */ @Data @NoArgsConstructor public class DashboardSummaryDto { + /** Total number of assets in the system. */ private long totalAssets; + /** Counts grouped by asset status. */ private List byStatus; + /** Counts grouped by asset type. */ private List byType; + /** Assets expiring within the next 30 days. */ private long expiringWithin30Days; + /** Assets whose warranty has already expired. */ private long alreadyExpired; + /** Open condition reports awaiting action. */ private long openConditionReports; + /** Unallocated laptops available for assignment. */ private long unallocatedLaptops; public DashboardSummaryDto(long totalAssets, @@ -37,7 +47,9 @@ public DashboardSummaryDto(long totalAssets, @Data @NoArgsConstructor public static class StatusCountDto { + /** Asset status bucket. */ private AssetStatus status; + /** Number of assets in this bucket. */ private long count; public StatusCountDto(AssetStatus status, long count) { @@ -49,7 +61,9 @@ public StatusCountDto(AssetStatus status, long count) { @Data @NoArgsConstructor public static class TypeCountDto { + /** Asset type bucket. */ private AssetType type; + /** Number of assets in this bucket. */ private long count; public TypeCountDto(AssetType type, long count) { diff --git a/backend/src/main/java/com/assettrack/dto/exception/ErrorResponse.java b/backend/src/main/java/com/assettrack/dto/exception/ErrorResponse.java index 08f4592..4c47b0a 100644 --- a/backend/src/main/java/com/assettrack/dto/exception/ErrorResponse.java +++ b/backend/src/main/java/com/assettrack/dto/exception/ErrorResponse.java @@ -3,6 +3,9 @@ import lombok.AllArgsConstructor; import lombok.Data; +/** + * Legacy error response used by older exception handlers. + */ @Data @AllArgsConstructor public class ErrorResponse { diff --git a/backend/src/main/java/com/assettrack/dto/notification/NotificationResponse.java b/backend/src/main/java/com/assettrack/dto/notification/NotificationResponse.java index de17cab..de99445 100644 --- a/backend/src/main/java/com/assettrack/dto/notification/NotificationResponse.java +++ b/backend/src/main/java/com/assettrack/dto/notification/NotificationResponse.java @@ -9,15 +9,24 @@ import com.fasterxml.jackson.annotation.JsonFormat; import java.util.UUID; +/** + * Notification response returned to the authenticated user. + */ @Data @NoArgsConstructor public class NotificationResponse { + /** Notification identifier. */ private UUID id; + /** Notification type. */ private NotificationType type; + /** Human-readable message. */ private String message; + /** Whether the notification has been read. */ private boolean read; + /** Related asset identifier, if any. */ private UUID assetId; + /** UTC creation timestamp. */ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") private LocalDateTime createdAt; diff --git a/backend/src/main/java/com/assettrack/dto/user/UpdateEmailRequest.java b/backend/src/main/java/com/assettrack/dto/user/UpdateEmailRequest.java index 0f6f10a..c1c84ab 100644 --- a/backend/src/main/java/com/assettrack/dto/user/UpdateEmailRequest.java +++ b/backend/src/main/java/com/assettrack/dto/user/UpdateEmailRequest.java @@ -4,13 +4,18 @@ import jakarta.validation.constraints.NotBlank; import lombok.Data; +/** + * Request payload for changing the authenticated user's email address. + */ @Data public class UpdateEmailRequest { + /** New email address to assign to the account. */ @NotBlank @Email private String newEmail; + /** Current password used to confirm the change. */ @NotBlank private String password; diff --git a/backend/src/main/java/com/assettrack/dto/user/UpdatePasswordRequest.java b/backend/src/main/java/com/assettrack/dto/user/UpdatePasswordRequest.java index 06eaefc..d30a8e2 100644 --- a/backend/src/main/java/com/assettrack/dto/user/UpdatePasswordRequest.java +++ b/backend/src/main/java/com/assettrack/dto/user/UpdatePasswordRequest.java @@ -4,16 +4,18 @@ import jakarta.validation.constraints.Pattern; import lombok.Data; +/** + * Request payload for changing the authenticated user's password. + */ @Data public class UpdatePasswordRequest { + /** Current password used for verification. */ @NotBlank - private String currentPassword ; + private String currentPassword; + /** New password to store for the account. */ @NotBlank - @Pattern( - regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$", - message = "Password must be at least 8 characters, contain uppercase, lowercase, and a number" - ) - private String newPassword; + @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$", message = "Password must be at least 8 characters, contain uppercase, lowercase, and a number") + private String newPassword; } diff --git a/backend/src/main/java/com/assettrack/dto/user/UserResponse.java b/backend/src/main/java/com/assettrack/dto/user/UserResponse.java index 09f90e0..62127f4 100644 --- a/backend/src/main/java/com/assettrack/dto/user/UserResponse.java +++ b/backend/src/main/java/com/assettrack/dto/user/UserResponse.java @@ -1,7 +1,5 @@ package com.assettrack.dto.user; - - import com.assettrack.domain.user.Role; import lombok.AllArgsConstructor; import lombok.Builder; @@ -12,19 +10,29 @@ import com.fasterxml.jackson.annotation.JsonFormat; import java.util.UUID; +/** + * Full user response returned by user and auth endpoints. + */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class UserResponse { + /** User identifier. */ private UUID id; + /** User email address. */ private String email; + /** Full display name. */ private String fullName; + /** Assigned account role. */ private Role role; + /** UTC creation timestamp. */ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") private LocalDateTime createdAt; + /** UTC last update timestamp. */ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") private LocalDateTime updatedAt; + /** Whether the account is active. */ private boolean active; } diff --git a/backend/src/main/java/com/assettrack/dto/user/UserSummary.java b/backend/src/main/java/com/assettrack/dto/user/UserSummary.java index f7c11b4..52562aa 100644 --- a/backend/src/main/java/com/assettrack/dto/user/UserSummary.java +++ b/backend/src/main/java/com/assettrack/dto/user/UserSummary.java @@ -8,13 +8,20 @@ import java.util.UUID; +/** + * Compact user representation used in nested responses. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class UserSummary { + /** User identifier. */ private UUID id; + /** User email address. */ private String email; + /** Full display name. */ private String fullName; + /** Assigned account role. */ private Role role; } \ No newline at end of file diff --git a/backend/src/main/java/com/assettrack/exception/ActiveUserDeletionException.java b/backend/src/main/java/com/assettrack/exception/ActiveUserDeletionException.java index f6ec33c..8329fbf 100644 --- a/backend/src/main/java/com/assettrack/exception/ActiveUserDeletionException.java +++ b/backend/src/main/java/com/assettrack/exception/ActiveUserDeletionException.java @@ -1,5 +1,8 @@ package com.assettrack.exception; +/** + * Exception thrown when trying to delete an account that is still active. + */ public class ActiveUserDeletionException extends BaseException { public ActiveUserDeletionException(String message) { super(message, 409); diff --git a/backend/src/main/java/com/assettrack/exception/BaseException.java b/backend/src/main/java/com/assettrack/exception/BaseException.java index 73bd243..1e25441 100644 --- a/backend/src/main/java/com/assettrack/exception/BaseException.java +++ b/backend/src/main/java/com/assettrack/exception/BaseException.java @@ -1,5 +1,8 @@ package com.assettrack.exception; +/** + * Base runtime exception carrying an HTTP status code. + */ public abstract class BaseException extends RuntimeException { private final int statusCode; diff --git a/backend/src/main/java/com/assettrack/exception/ConflictException.java b/backend/src/main/java/com/assettrack/exception/ConflictException.java index 65685dc..8bf3c3f 100644 --- a/backend/src/main/java/com/assettrack/exception/ConflictException.java +++ b/backend/src/main/java/com/assettrack/exception/ConflictException.java @@ -1,5 +1,8 @@ package com.assettrack.exception; +/** + * Exception thrown when a request conflicts with the current resource state. + */ public class ConflictException extends BaseException { public ConflictException(String message) { super(message, 409); diff --git a/backend/src/main/java/com/assettrack/exception/DuplicateSerialNumberException.java b/backend/src/main/java/com/assettrack/exception/DuplicateSerialNumberException.java index c70c2da..8e5cba8 100644 --- a/backend/src/main/java/com/assettrack/exception/DuplicateSerialNumberException.java +++ b/backend/src/main/java/com/assettrack/exception/DuplicateSerialNumberException.java @@ -1,5 +1,10 @@ package com.assettrack.exception; -public class DuplicateSerialNumberException extends BaseException{ - public DuplicateSerialNumberException(String message){super(message,409);} +/** + * Exception thrown when an asset serial number is already registered. + */ +public class DuplicateSerialNumberException extends BaseException { + public DuplicateSerialNumberException(String message) { + super(message, 409); + } } diff --git a/backend/src/main/java/com/assettrack/exception/EmailAlreadyExistsException.java b/backend/src/main/java/com/assettrack/exception/EmailAlreadyExistsException.java index 27bb13c..58acc4e 100644 --- a/backend/src/main/java/com/assettrack/exception/EmailAlreadyExistsException.java +++ b/backend/src/main/java/com/assettrack/exception/EmailAlreadyExistsException.java @@ -1,5 +1,8 @@ package com.assettrack.exception; +/** + * Exception thrown when an email address is already in use. + */ public class EmailAlreadyExistsException extends BaseException { public EmailAlreadyExistsException(String message) { super(message, 409); diff --git a/backend/src/main/java/com/assettrack/exception/InvalidPasswordException.java b/backend/src/main/java/com/assettrack/exception/InvalidPasswordException.java index e51a89b..92f4400 100644 --- a/backend/src/main/java/com/assettrack/exception/InvalidPasswordException.java +++ b/backend/src/main/java/com/assettrack/exception/InvalidPasswordException.java @@ -1,9 +1,10 @@ package com.assettrack.exception; +/** + * Exception thrown when a password check fails. + */ public class InvalidPasswordException extends BaseException { public InvalidPasswordException(String message) { super(message, 400); } } - - diff --git a/backend/src/main/java/com/assettrack/exception/InvalidRoleException.java b/backend/src/main/java/com/assettrack/exception/InvalidRoleException.java index 64c4f93..40bb392 100644 --- a/backend/src/main/java/com/assettrack/exception/InvalidRoleException.java +++ b/backend/src/main/java/com/assettrack/exception/InvalidRoleException.java @@ -1,5 +1,10 @@ package com.assettrack.exception; -public class InvalidRoleException extends BaseException{ - public InvalidRoleException(String message){super(message,400);} +/** + * Exception thrown when a supplied role value is invalid. + */ +public class InvalidRoleException extends BaseException { + public InvalidRoleException(String message) { + super(message, 400); + } } diff --git a/backend/src/main/java/com/assettrack/exception/ResourceNotFoundException.java b/backend/src/main/java/com/assettrack/exception/ResourceNotFoundException.java index 3ca316d..a7caac1 100644 --- a/backend/src/main/java/com/assettrack/exception/ResourceNotFoundException.java +++ b/backend/src/main/java/com/assettrack/exception/ResourceNotFoundException.java @@ -1,5 +1,8 @@ package com.assettrack.exception; +/** + * Exception thrown when a requested resource cannot be found. + */ public class ResourceNotFoundException extends BaseException { public ResourceNotFoundException(String message) { super(message, 404); diff --git a/backend/src/main/java/com/assettrack/exception/SelfOperationException.java b/backend/src/main/java/com/assettrack/exception/SelfOperationException.java index 7afe33c..1a0a619 100644 --- a/backend/src/main/java/com/assettrack/exception/SelfOperationException.java +++ b/backend/src/main/java/com/assettrack/exception/SelfOperationException.java @@ -1,5 +1,10 @@ package com.assettrack.exception; -public class SelfOperationException extends BaseException{ - public SelfOperationException(String message){super(message,403);} +/** + * Exception thrown when an admin attempts to operate on their own account. + */ +public class SelfOperationException extends BaseException { + public SelfOperationException(String message) { + super(message, 403); + } } diff --git a/backend/src/main/java/com/assettrack/mapper/allocation/AllocationMapper.java b/backend/src/main/java/com/assettrack/mapper/allocation/AllocationMapper.java index 10bde3d..82aeed8 100644 --- a/backend/src/main/java/com/assettrack/mapper/allocation/AllocationMapper.java +++ b/backend/src/main/java/com/assettrack/mapper/allocation/AllocationMapper.java @@ -9,6 +9,9 @@ import java.time.LocalDateTime; import java.util.List; +/** + * Mapper for allocation entities and DTOs. + */ @Mapper(componentModel = "spring", uses = { com.assettrack.mapper.user.UserMapper.class, com.assettrack.mapper.asset.AssetMapper.class }) public interface AllocationMapper { diff --git a/backend/src/main/java/com/assettrack/mapper/asset/AssetMapper.java b/backend/src/main/java/com/assettrack/mapper/asset/AssetMapper.java index a5915dd..73b1b05 100644 --- a/backend/src/main/java/com/assettrack/mapper/asset/AssetMapper.java +++ b/backend/src/main/java/com/assettrack/mapper/asset/AssetMapper.java @@ -15,6 +15,9 @@ import java.time.LocalDate; import java.time.temporal.ChronoUnit; +/** + * Mapper for asset and condition report entities. + */ @Mapper(componentModel = "spring", uses = { UserMapper.class }) public abstract class AssetMapper { diff --git a/backend/src/main/java/com/assettrack/mapper/auth/AuthMapper.java b/backend/src/main/java/com/assettrack/mapper/auth/AuthMapper.java index 46592c7..0d5c11a 100644 --- a/backend/src/main/java/com/assettrack/mapper/auth/AuthMapper.java +++ b/backend/src/main/java/com/assettrack/mapper/auth/AuthMapper.java @@ -7,6 +7,9 @@ import com.assettrack.mapper.user.UserMapper; import org.springframework.beans.factory.annotation.Autowired; +/** + * Mapper for authentication responses. + */ @Mapper(componentModel = "spring", uses = { UserMapper.class }) public abstract class AuthMapper { @Autowired diff --git a/backend/src/main/java/com/assettrack/mapper/user/UserMapper.java b/backend/src/main/java/com/assettrack/mapper/user/UserMapper.java index c417757..0d72639 100644 --- a/backend/src/main/java/com/assettrack/mapper/user/UserMapper.java +++ b/backend/src/main/java/com/assettrack/mapper/user/UserMapper.java @@ -6,6 +6,9 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; +/** + * Mapper for user entities and user-facing DTOs. + */ @Mapper(componentModel = "spring") public interface UserMapper { @Mapping(target = "fullName", expression = "java(resolveFullName(user))") diff --git a/backend/src/main/java/com/assettrack/repository/asset/AssetAllocationRepository.java b/backend/src/main/java/com/assettrack/repository/asset/AssetAllocationRepository.java index 0cb091d..5bb26a9 100644 --- a/backend/src/main/java/com/assettrack/repository/asset/AssetAllocationRepository.java +++ b/backend/src/main/java/com/assettrack/repository/asset/AssetAllocationRepository.java @@ -8,10 +8,14 @@ import java.util.Optional; +/** + * Repository for asset allocation records. + */ @Repository public interface AssetAllocationRepository extends JpaRepository { Page findByAssetId(java.util.UUID assetId, Pageable pageable); + Optional findByAssetIdAndReturnDateIsNull(java.util.UUID assetId); boolean existsByAssetIdAndUserIdAndReturnDateIsNull(java.util.UUID assetId, java.util.UUID userId); diff --git a/backend/src/main/java/com/assettrack/repository/asset/AssetRepository.java b/backend/src/main/java/com/assettrack/repository/asset/AssetRepository.java index c58b24c..a2735d5 100644 --- a/backend/src/main/java/com/assettrack/repository/asset/AssetRepository.java +++ b/backend/src/main/java/com/assettrack/repository/asset/AssetRepository.java @@ -14,6 +14,9 @@ import java.util.List; import java.util.Optional; +/** + * Repository for tracked assets. + */ @Repository public interface AssetRepository extends JpaRepository, JpaSpecificationExecutor { @@ -35,11 +38,13 @@ public interface AssetRepository extends JpaRepository, J interface StatusCount { AssetStatus getStatus(); + Long getCount(); } interface TypeCount { AssetType getType(); + Long getCount(); } } diff --git a/backend/src/main/java/com/assettrack/repository/asset/ConditionReportRepository.java b/backend/src/main/java/com/assettrack/repository/asset/ConditionReportRepository.java index b31b4ac..6160698 100644 --- a/backend/src/main/java/com/assettrack/repository/asset/ConditionReportRepository.java +++ b/backend/src/main/java/com/assettrack/repository/asset/ConditionReportRepository.java @@ -9,6 +9,9 @@ import java.util.List; +/** + * Repository for asset condition reports. + */ @Repository public interface ConditionReportRepository extends JpaRepository { @@ -16,7 +19,8 @@ public interface ConditionReportRepository extends JpaRepository findByAssetIdOrderByReportDateDesc(java.util.UUID assetId, Pageable pageable); - Page findByAssetIdAndReportedByIdOrderByReportDateDesc(java.util.UUID assetId, java.util.UUID userId, Pageable pageable); + Page findByAssetIdAndReportedByIdOrderByReportDateDesc(java.util.UUID assetId, + java.util.UUID userId, Pageable pageable); Page findByReportedByIdOrderByReportDateDesc(java.util.UUID userId, Pageable pageable); diff --git a/backend/src/main/java/com/assettrack/repository/notification/NotificationRepository.java b/backend/src/main/java/com/assettrack/repository/notification/NotificationRepository.java index 3b98043..063c6b5 100644 --- a/backend/src/main/java/com/assettrack/repository/notification/NotificationRepository.java +++ b/backend/src/main/java/com/assettrack/repository/notification/NotificationRepository.java @@ -6,9 +6,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.util.List; import java.util.Optional; +/** + * Repository for user notification records. + */ @Repository public interface NotificationRepository extends JpaRepository { diff --git a/backend/src/main/java/com/assettrack/repository/user/UserRepository.java b/backend/src/main/java/com/assettrack/repository/user/UserRepository.java index cc2e343..20b01b4 100644 --- a/backend/src/main/java/com/assettrack/repository/user/UserRepository.java +++ b/backend/src/main/java/com/assettrack/repository/user/UserRepository.java @@ -7,6 +7,9 @@ import org.springframework.stereotype.Repository; import java.util.Optional; +/** + * Repository for application user accounts. + */ @Repository public interface UserRepository extends JpaRepository { diff --git a/backend/src/main/java/com/assettrack/security/config/CorsProperties.java b/backend/src/main/java/com/assettrack/security/config/CorsProperties.java new file mode 100644 index 0000000..2e794ec --- /dev/null +++ b/backend/src/main/java/com/assettrack/security/config/CorsProperties.java @@ -0,0 +1,30 @@ +package com.assettrack.security.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * CORS configuration properties loaded from application.yml. + * + * Allows externalization of allowed origins for flexibility across environments + * (development, staging, production). + * + * Example in application.yml: + * + *
+ * cors:
+ *   allowed-origins: http://localhost:3000,http://localhost:5173,https://app.example.com
+ * 
+ */ +@Data +@Component +@ConfigurationProperties(prefix = "cors") +public class CorsProperties { + + /** + * Comma-separated list of allowed origins for CORS requests. + * Defaults to localhost development ports if not specified. + */ + private String allowedOrigins = "http://localhost:3000,http://localhost:5173,http://localhost:8080"; +} diff --git a/backend/src/main/java/com/assettrack/security/config/CustomAccessDeniedHandler.java b/backend/src/main/java/com/assettrack/security/config/CustomAccessDeniedHandler.java index a0bc3b6..028b640 100644 --- a/backend/src/main/java/com/assettrack/security/config/CustomAccessDeniedHandler.java +++ b/backend/src/main/java/com/assettrack/security/config/CustomAccessDeniedHandler.java @@ -13,6 +13,9 @@ import java.io.IOException; import java.time.Instant; +/** + * Handles 403 responses for access denied requests. + */ @Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { @@ -23,7 +26,8 @@ public CustomAccessDeniedHandler(ObjectMapper objectMapper) { } @Override - public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { + public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException { ApiError apiError = ApiError.builder() .timestamp(Instant.now().toString()) .status(HttpStatus.FORBIDDEN.value()) diff --git a/backend/src/main/java/com/assettrack/security/config/CustomAuthenticationEntryPoint.java b/backend/src/main/java/com/assettrack/security/config/CustomAuthenticationEntryPoint.java index 5e134f8..023e270 100644 --- a/backend/src/main/java/com/assettrack/security/config/CustomAuthenticationEntryPoint.java +++ b/backend/src/main/java/com/assettrack/security/config/CustomAuthenticationEntryPoint.java @@ -13,6 +13,9 @@ import java.io.IOException; import java.time.Instant; +/** + * Handles 401 responses for unauthenticated requests. + */ @Component public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @@ -23,7 +26,8 @@ public CustomAuthenticationEntryPoint(ObjectMapper objectMapper) { } @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException { ApiError apiError = ApiError.builder() .timestamp(Instant.now().toString()) .status(HttpStatus.UNAUTHORIZED.value()) diff --git a/backend/src/main/java/com/assettrack/security/config/JwtAuthenticationConverter.java b/backend/src/main/java/com/assettrack/security/config/JwtAuthenticationConverter.java index 38f9ad2..db76a8a 100644 --- a/backend/src/main/java/com/assettrack/security/config/JwtAuthenticationConverter.java +++ b/backend/src/main/java/com/assettrack/security/config/JwtAuthenticationConverter.java @@ -10,6 +10,9 @@ import java.util.Collection; import java.util.Collections; +/** + * Converts JWT claims into Spring Security authentication tokens. + */ public class JwtAuthenticationConverter implements Converter { @Override diff --git a/backend/src/main/java/com/assettrack/security/config/RsaKeyProperties.java b/backend/src/main/java/com/assettrack/security/config/RsaKeyProperties.java index 92b0683..92a5e05 100644 --- a/backend/src/main/java/com/assettrack/security/config/RsaKeyProperties.java +++ b/backend/src/main/java/com/assettrack/security/config/RsaKeyProperties.java @@ -20,6 +20,9 @@ import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +/** + * Loads RSA key resources and exposes JWT encoder and decoder beans. + */ @Configuration @ConfigurationProperties(prefix = "rsa") public class RsaKeyProperties { diff --git a/backend/src/main/java/com/assettrack/security/config/SecurityConfig.java b/backend/src/main/java/com/assettrack/security/config/SecurityConfig.java index c7bc46c..9a1968e 100644 --- a/backend/src/main/java/com/assettrack/security/config/SecurityConfig.java +++ b/backend/src/main/java/com/assettrack/security/config/SecurityConfig.java @@ -13,73 +13,88 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfigurationSource; +/** + * Spring Security configuration for AssetTrack. + * + * Configures: + * - OAuth2 JWT resource server authentication + * - Method-level authorization with @PreAuthorize + * - CORS with externalized origin configuration + * - Stateless session management + * - Custom exception handling for authentication/authorization failures + */ @Configuration @EnableMethodSecurity public class SecurityConfig { + private final CorsProperties corsProperties; - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http, - CorsConfigurationSource corsConfigurationSource, - CustomAuthenticationEntryPoint authenticationEntryPoint, - CustomAccessDeniedHandler accessDeniedHandler) throws Exception { - return http - .cors(cors -> cors.configurationSource(corsConfigurationSource)) - .authorizeHttpRequests(auth -> auth - // Public endpoints - .requestMatchers("/auth/signup", "/auth/login").permitAll() - .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**").permitAll() - .requestMatchers("/actuator/health").permitAll() + public SecurityConfig(CorsProperties corsProperties) { + this.corsProperties = corsProperties; + } - // Secure endpoints - rely on @PreAuthorize on controllers for granular roles - .requestMatchers("/**").authenticated() - ) - .csrf(AbstractHttpConfigurer::disable) - .formLogin(AbstractHttpConfigurer::disable) - .logout(AbstractHttpConfigurer::disable) - .httpBasic(AbstractHttpConfigurer::disable) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .exceptionHandling(exceptions -> exceptions - .authenticationEntryPoint(authenticationEntryPoint) - .accessDeniedHandler(accessDeniedHandler) - ) - .oauth2ResourceServer(oauth2 -> oauth2 - .jwt(jwt -> jwt.jwtAuthenticationConverter(new JwtAuthenticationConverter())) - .authenticationEntryPoint(authenticationEntryPoint) - .accessDeniedHandler(accessDeniedHandler) - ) - .build(); - } + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, + CorsConfigurationSource corsConfigurationSource, + CustomAuthenticationEntryPoint authenticationEntryPoint, + CustomAccessDeniedHandler accessDeniedHandler) throws Exception { + return http + .cors(cors -> cors.configurationSource(corsConfigurationSource)) + .authorizeHttpRequests(auth -> auth + // Public endpoints + .requestMatchers("/auth/signup", "/auth/login").permitAll() + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", + "/swagger-resources/**", "/webjars/**") + .permitAll() + .requestMatchers("/actuator/health").permitAll() - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { - return authenticationConfiguration.getAuthenticationManager(); - } + // Secure endpoints - rely on @PreAuthorize on controllers for granular + // roles + .requestMatchers("/**").authenticated()) + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling(exceptions -> exceptions + .authenticationEntryPoint(authenticationEntryPoint) + .accessDeniedHandler(accessDeniedHandler)) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtAuthenticationConverter( + new JwtAuthenticationConverter())) + .authenticationEntryPoint(authenticationEntryPoint) + .accessDeniedHandler(accessDeniedHandler)) + .build(); + } - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(12); - } + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) + throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } - @Bean - public CorsConfigurationSource corsConfigurationSource() { - org.springframework.web.cors.CorsConfiguration configuration = new org.springframework.web.cors.CorsConfiguration(); - - configuration.setAllowedOrigins(java.util.List.of( - "http://shehabtech.me", - "https://shehabtech.me", - "http://localhost:8080", - "http://localhost:3000", - "http://localhost:5173" - )); - - configuration.setAllowedMethods(java.util.List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); - configuration.setAllowedHeaders(java.util.List.of("Authorization", "Content-Type", "X-Requested-With", "Accept", "Origin")); - configuration.setExposedHeaders(java.util.List.of("Authorization")); - configuration.setAllowCredentials(true); - configuration.setMaxAge(3600L); + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(12); + } - org.springframework.web.cors.UrlBasedCorsConfigurationSource source = new org.springframework.web.cors.UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - return source; - } + @Bean + public CorsConfigurationSource corsConfigurationSource() { + org.springframework.web.cors.CorsConfiguration configuration = new org.springframework.web.cors.CorsConfiguration(); + + // Parse externalized origins from properties + String[] origins = corsProperties.getAllowedOrigins().split(","); + configuration.setAllowedOrigins(java.util.Arrays.asList(origins)); + + configuration.setAllowedMethods(java.util.List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(java.util.List.of("Authorization", "Content-Type", "X-Requested-With", + "Accept", "Origin")); + configuration.setExposedHeaders(java.util.List.of("Authorization")); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); + + org.springframework.web.cors.UrlBasedCorsConfigurationSource source = new org.springframework.web.cors.UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } } diff --git a/backend/src/main/java/com/assettrack/security/service/JwtService.java b/backend/src/main/java/com/assettrack/security/service/JwtService.java index 49cbb2f..685f7e2 100644 --- a/backend/src/main/java/com/assettrack/security/service/JwtService.java +++ b/backend/src/main/java/com/assettrack/security/service/JwtService.java @@ -8,6 +8,9 @@ import java.time.Instant; import java.util.Map; +/** + * JWT utility service for token creation and validation. + */ @Service public class JwtService { @@ -22,7 +25,6 @@ public JwtService(JwtEncoder jwtEncoder, JwtDecoder jwtDecoder) { this.jwtDecoder = jwtDecoder; } - public String generateToken(Map claims, Duration ttl) { Instant now = Instant.now(); Instant exp = now.plus(ttl); @@ -43,8 +45,6 @@ public String generateToken(Map claims, Duration ttl) { return jwtEncoder.encode(JwtEncoderParameters.from(claimSet)).getTokenValue(); } - - public Jwt validateToken(String token) { return jwtDecoder.decode(token); } diff --git a/backend/src/main/java/com/assettrack/security/service/UserDetailsServiceImpl.java b/backend/src/main/java/com/assettrack/security/service/UserDetailsServiceImpl.java index d7f4ade..385ee77 100644 --- a/backend/src/main/java/com/assettrack/security/service/UserDetailsServiceImpl.java +++ b/backend/src/main/java/com/assettrack/security/service/UserDetailsServiceImpl.java @@ -11,27 +11,28 @@ import java.util.List; +/** + * Loads users from the database for Spring Security authentication. + */ @Service @RequiredArgsConstructor public class UserDetailsServiceImpl implements UserDetailsService { - private final UserRepository userRepository; + private final UserRepository userRepository; - @Override - public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - User user = userRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException( - "User not found with email: " + email - )); + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException( + "User not found with email: " + email)); - return new org.springframework.security.core.userdetails.User( - user.getEmail(), - user.getPasswordHash(), - user.isActive(), - true, - true, - true, - List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())) - ); - } + return new org.springframework.security.core.userdetails.User( + user.getEmail(), + user.getPasswordHash(), + user.isActive(), + true, + true, + true, + List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()))); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/assettrack/service/allocation/AllocationService.java b/backend/src/main/java/com/assettrack/service/allocation/AllocationService.java index 00c52b2..805f1b2 100644 --- a/backend/src/main/java/com/assettrack/service/allocation/AllocationService.java +++ b/backend/src/main/java/com/assettrack/service/allocation/AllocationService.java @@ -13,26 +13,28 @@ import com.assettrack.repository.asset.AssetRepository; import com.assettrack.repository.user.UserRepository; import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import com.assettrack.exception.ResourceNotFoundException; import org.springframework.transaction.annotation.Transactional; +import lombok.extern.slf4j.Slf4j; import java.time.LocalDateTime; import java.util.UUID; +/** + * Allocation service for assigning and returning assets. + */ @Service @RequiredArgsConstructor +@Slf4j public class AllocationService implements IAllocationService { private final AssetRepository assetRepository; private final UserRepository userRepository; private final AssetAllocationRepository allocationRepository; private final AllocationMapper allocationMapper; - private static final Logger log = LoggerFactory.getLogger(AllocationService.class); /** * Allocates an available asset to a user. @@ -66,6 +68,7 @@ public AllocationResponseDto allocate(UUID assetId, AllocationRequestDto dto) { } public AllocationResponseDto getAllocationById(UUID allocationId, UUID assetId) { + log.debug("Loading allocation {} for asset {}", allocationId, assetId); AssetAllocation assetAllocation = allocationRepository.findById(allocationId) .orElseThrow(() -> new ResourceNotFoundException("Allocation not found")); if (!assetAllocation.getAsset().getId().equals(assetId)) { @@ -99,6 +102,7 @@ public void deallocate(java.util.UUID assetId) { asset.setStatus(AssetStatus.AVAILABLE); assetRepository.save(asset); allocationRepository.save(allocation); + log.info("Asset {} deallocated successfully", assetId); } /** @@ -112,6 +116,7 @@ public void deallocate(java.util.UUID assetId) { @Override @Transactional(readOnly = true) public Page getAllocationHistory(java.util.UUID assetId, Pageable pageable) { + log.debug("Loading allocation history for asset {}", assetId); assetRepository.findById(assetId) .orElseThrow(() -> new ResourceNotFoundException("Asset is not found")); return allocationRepository.findByAssetId(assetId, pageable) diff --git a/backend/src/main/java/com/assettrack/service/allocation/IAllocationService.java b/backend/src/main/java/com/assettrack/service/allocation/IAllocationService.java index 148224e..0b4f528 100644 --- a/backend/src/main/java/com/assettrack/service/allocation/IAllocationService.java +++ b/backend/src/main/java/com/assettrack/service/allocation/IAllocationService.java @@ -9,6 +9,9 @@ import java.util.UUID; +/** + * Contract for asset allocation operations. + */ public interface IAllocationService { AllocationResponseDto allocate(java.util.UUID assetId, AllocationRequestDto dto); diff --git a/backend/src/main/java/com/assettrack/service/asset/AssetService.java b/backend/src/main/java/com/assettrack/service/asset/AssetService.java index d41af63..d4469c5 100644 --- a/backend/src/main/java/com/assettrack/service/asset/AssetService.java +++ b/backend/src/main/java/com/assettrack/service/asset/AssetService.java @@ -20,8 +20,7 @@ import com.assettrack.repository.user.UserRepository; import com.assettrack.security.util.SecurityUtils; import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; @@ -31,7 +30,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.List; import java.util.UUID; /** @@ -39,6 +37,7 @@ */ @Service @RequiredArgsConstructor +@Slf4j public class AssetService implements IAssetService { private final AssetRepository assetRepository; @@ -47,7 +46,6 @@ public class AssetService implements IAssetService { private final UserRepository userRepository; private final AssetMapper assetMapper; private final SecurityUtils securityUtils; - private static final Logger log = LoggerFactory.getLogger(AssetService.class); // ──────────────────────────── Asset Search ──────────────────────────── /** @@ -128,7 +126,8 @@ public ConditionReportResponse createConditionReport(java.util.UUID assetId, * Returns all condition reports visible to the current user. */ @Transactional(readOnly = true) - public org.springframework.data.domain.Page getConditionReports(Authentication authentication, org.springframework.data.domain.Pageable pageable) { + public org.springframework.data.domain.Page getConditionReports( + Authentication authentication, org.springframework.data.domain.Pageable pageable) { java.util.UUID userId = securityUtils.getCurrentUserId(authentication); org.springframework.data.domain.Page reports = securityUtils.isManagerOrAdmin(authentication) ? conditionReportRepository.findAllByOrderByReportDateDesc(pageable) @@ -150,7 +149,8 @@ public org.springframework.data.domain.Page getReportsB java.util.UUID userId = securityUtils.getCurrentUserId(authentication); org.springframework.data.domain.Page reports = securityUtils.isManagerOrAdmin(authentication) ? conditionReportRepository.findByAssetIdOrderByReportDateDesc(assetId, pageable) - : conditionReportRepository.findByAssetIdAndReportedByIdOrderByReportDateDesc(assetId, userId, pageable); + : conditionReportRepository.findByAssetIdAndReportedByIdOrderByReportDateDesc(assetId, userId, + pageable); return reports.map(assetMapper::toResponse); } diff --git a/backend/src/main/java/com/assettrack/service/asset/IAssetService.java b/backend/src/main/java/com/assettrack/service/asset/IAssetService.java index 0f77557..1be1015 100644 --- a/backend/src/main/java/com/assettrack/service/asset/IAssetService.java +++ b/backend/src/main/java/com/assettrack/service/asset/IAssetService.java @@ -11,34 +11,36 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.security.core.Authentication; - -import java.util.List; import java.util.UUID; +/** + * Contract for asset lifecycle, search, and condition-report operations. + */ public interface IAssetService { - Page searchAssets(AssetStatus status, AssetType type, String brand, String serialNumber, - Pageable pageable); + Page searchAssets(AssetStatus status, AssetType type, String brand, String serialNumber, + Pageable pageable); - ConditionReportResponse createConditionReport(CreateConditionReportRequest request, Authentication authentication); + ConditionReportResponse createConditionReport(CreateConditionReportRequest request, + Authentication authentication); - ConditionReportResponse createConditionReport(UUID assetId, String description, ConditionSeverity severity, - Authentication authentication); + ConditionReportResponse createConditionReport(UUID assetId, String description, ConditionSeverity severity, + Authentication authentication); - Page getConditionReports(Authentication authentication, Pageable pageable); + Page getConditionReports(Authentication authentication, Pageable pageable); - Page getReportsByAsset(UUID assetId, Authentication authentication, Pageable pageable); + Page getReportsByAsset(UUID assetId, Authentication authentication, Pageable pageable); - ConditionReportResponse getReportById(UUID reportId, Authentication authentication); + ConditionReportResponse getReportById(UUID reportId, Authentication authentication); - ConditionReportResponse resolveReport(UUID reportId); + ConditionReportResponse resolveReport(UUID reportId); - AssetResponse registerAsset(CreateAssetRequest request); + AssetResponse registerAsset(CreateAssetRequest request); - AssetResponse getAssetById(UUID id); + AssetResponse getAssetById(UUID id); - AssetResponse updateAsset(UUID id, UpdateAssetRequest request); + AssetResponse updateAsset(UUID id, UpdateAssetRequest request); - void deleteAsset(UUID id); + void deleteAsset(UUID id); - void expireWarrantiedAssets(); + void expireWarrantiedAssets(); } \ No newline at end of file diff --git a/backend/src/main/java/com/assettrack/service/asset/WarrantyScheduler.java b/backend/src/main/java/com/assettrack/service/asset/WarrantyScheduler.java index e658598..c6adbe6 100644 --- a/backend/src/main/java/com/assettrack/service/asset/WarrantyScheduler.java +++ b/backend/src/main/java/com/assettrack/service/asset/WarrantyScheduler.java @@ -1,16 +1,18 @@ package com.assettrack.service.asset; import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +/** + * Scheduled job that marks expiring warranties as expired. + */ @Component @RequiredArgsConstructor +@Slf4j public class WarrantyScheduler { private final IAssetService assetService; - private static final Logger log = LoggerFactory.getLogger(WarrantyScheduler.class); @Scheduled(cron = "0 0 0 * * *") public void checkWarrantyExpirations() { diff --git a/backend/src/main/java/com/assettrack/service/auth/AuthService.java b/backend/src/main/java/com/assettrack/service/auth/AuthService.java index 5635502..8612534 100644 --- a/backend/src/main/java/com/assettrack/service/auth/AuthService.java +++ b/backend/src/main/java/com/assettrack/service/auth/AuthService.java @@ -10,20 +10,25 @@ import com.assettrack.mapper.auth.AuthMapper; import com.assettrack.repository.user.UserRepository; import com.assettrack.security.service.JwtService; + import lombok.RequiredArgsConstructor; import com.assettrack.dto.user.UserResponse; import com.assettrack.mapper.user.UserMapper; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.time.Duration; import java.util.Map; +/** + * Authentication service for registering and authenticating users. + */ @Service @RequiredArgsConstructor +@Slf4j public class AuthService implements IAuthService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; @@ -32,6 +37,12 @@ public class AuthService implements IAuthService { private final AuthMapper authMapper; private final UserMapper userMapper; + /** + * Generates a signed JWT for the given user. + * + * @param user authenticated user + * @return signed JWT token + */ private String generateToken(User user) { return jwtService.generateToken( Map.of( @@ -39,13 +50,22 @@ private String generateToken(User user) { "userId", user.getId()), Duration.ofHours(24)); } + + /** + * Registers a new developer account. + * + * @param request sign-up payload + * @return created user response + */ public UserResponse register(SignupRequest request) { + log.info("Registering user with email {}", request.getEmail()); if (userRepository.existsByEmail(request.getEmail())) { + log.warn("Registration rejected because email already exists: {}", request.getEmail()); throw new EmailAlreadyExistsException("Email already in use"); } User user = new User(); user.setEmail(request.getEmail()); - + String fullName = request.getFullName(); if (fullName != null) { String[] parts = fullName.split(" ", 2); @@ -54,19 +74,28 @@ public UserResponse register(SignupRequest request) { user.setLastName(parts[1]); } } - + user.setPasswordHash(passwordEncoder.encode(request.getPassword())); user.setRole(Role.DEVELOPER); User saved = userRepository.save(user); + log.info("User registered successfully with id {} and email {}", saved.getId(), saved.getEmail()); return userMapper.toResponse(saved); } + /** + * Authenticates a user and returns an access token. + * + * @param request login payload + * @return authentication response + */ public AuthResponse login(LoginRequest request) { + log.info("Authentication attempt for email {}", request.getEmail()); authenticationManager .authenticate(new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword())); User user = userRepository.findByEmail(request.getEmail()) .orElseThrow(() -> new ResourceNotFoundException("User not found")); String token = generateToken(user); + log.info("Authentication succeeded for user {} ({})", user.getEmail(), user.getId()); return authMapper.toResponse(token, user); } } \ No newline at end of file diff --git a/backend/src/main/java/com/assettrack/service/auth/IAuthService.java b/backend/src/main/java/com/assettrack/service/auth/IAuthService.java index 761bf74..f5d5898 100644 --- a/backend/src/main/java/com/assettrack/service/auth/IAuthService.java +++ b/backend/src/main/java/com/assettrack/service/auth/IAuthService.java @@ -6,6 +6,9 @@ import com.assettrack.dto.user.UserResponse; +/** + * Contract for authentication operations. + */ public interface IAuthService { UserResponse register(SignupRequest request); diff --git a/backend/src/main/java/com/assettrack/service/dashboard/DashboardService.java b/backend/src/main/java/com/assettrack/service/dashboard/DashboardService.java index 9b53152..f4fea58 100644 --- a/backend/src/main/java/com/assettrack/service/dashboard/DashboardService.java +++ b/backend/src/main/java/com/assettrack/service/dashboard/DashboardService.java @@ -18,9 +18,14 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +/** + * Dashboard service for aggregate inventory and analytics queries. + */ @Service @RequiredArgsConstructor +@Slf4j public class DashboardService implements IDashboardService { private final AssetRepository assetRepository; @@ -28,6 +33,7 @@ public class DashboardService implements IDashboardService { private final UserMapper userMapper; public DashboardSummaryDto getSummary() { + log.debug("Building dashboard summary"); long totalAssets = assetRepository.count(); Map statusCounts = assetRepository.countByStatus().stream() @@ -58,13 +64,17 @@ public DashboardSummaryDto getSummary() { @Transactional(readOnly = true) public SpareAssetResponse getQuickSpareLaptop() { + log.debug("Fetching quick spare laptop"); return assetRepository .findFirstByTypeAndStatusOrderByCreatedAtAsc(AssetType.LAPTOP, AssetStatus.AVAILABLE) .map(asset -> new SpareAssetResponse( assetMapper.toResponse(asset), resolveLastOwner(asset), resolveLastDeallocatedAt(asset))) - .orElseThrow(() -> new ResourceNotFoundException("No available spare laptop found.")); + .orElseThrow(() -> { + log.warn("No available spare laptop found"); + return new ResourceNotFoundException("No available spare laptop found."); + }); } private UserSummary resolveLastOwner(com.assettrack.domain.asset.Asset asset) { diff --git a/backend/src/main/java/com/assettrack/service/dashboard/IDashboardService.java b/backend/src/main/java/com/assettrack/service/dashboard/IDashboardService.java index 599eb5f..9ce3e20 100644 --- a/backend/src/main/java/com/assettrack/service/dashboard/IDashboardService.java +++ b/backend/src/main/java/com/assettrack/service/dashboard/IDashboardService.java @@ -3,6 +3,9 @@ import com.assettrack.dto.asset.SpareAssetResponse; import com.assettrack.dto.dashboard.DashboardSummaryDto; +/** + * Contract for dashboard summary and quick spare asset lookups. + */ public interface IDashboardService { DashboardSummaryDto getSummary(); diff --git a/backend/src/main/java/com/assettrack/service/notification/AlertService.java b/backend/src/main/java/com/assettrack/service/notification/AlertService.java index dca9b19..b0db993 100644 --- a/backend/src/main/java/com/assettrack/service/notification/AlertService.java +++ b/backend/src/main/java/com/assettrack/service/notification/AlertService.java @@ -9,6 +9,9 @@ import java.util.List; +/** + * Scheduled alert service for low-stock notifications. + */ @Service @RequiredArgsConstructor @Slf4j diff --git a/backend/src/main/java/com/assettrack/service/notification/EmailNotificationService.java b/backend/src/main/java/com/assettrack/service/notification/EmailNotificationService.java index bcb4063..9f9fb05 100644 --- a/backend/src/main/java/com/assettrack/service/notification/EmailNotificationService.java +++ b/backend/src/main/java/com/assettrack/service/notification/EmailNotificationService.java @@ -9,6 +9,9 @@ import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; +/** + * Email notification service used by alerting workflows. + */ @Service @RequiredArgsConstructor @Slf4j diff --git a/backend/src/main/java/com/assettrack/service/notification/IAlertService.java b/backend/src/main/java/com/assettrack/service/notification/IAlertService.java index b67c459..d17b39b 100644 --- a/backend/src/main/java/com/assettrack/service/notification/IAlertService.java +++ b/backend/src/main/java/com/assettrack/service/notification/IAlertService.java @@ -1,5 +1,8 @@ package com.assettrack.service.notification; +/** + * Contract for stock alert monitoring jobs. + */ public interface IAlertService { void checkLowStockAndAlert(); } \ No newline at end of file diff --git a/backend/src/main/java/com/assettrack/service/notification/IEmailNotificationService.java b/backend/src/main/java/com/assettrack/service/notification/IEmailNotificationService.java index a7df791..0d18374 100644 --- a/backend/src/main/java/com/assettrack/service/notification/IEmailNotificationService.java +++ b/backend/src/main/java/com/assettrack/service/notification/IEmailNotificationService.java @@ -1,5 +1,8 @@ package com.assettrack.service.notification; +/** + * Contract for sending notification emails. + */ public interface IEmailNotificationService { void sendEmail(String to, String subject, String text, boolean isHtml); } \ No newline at end of file diff --git a/backend/src/main/java/com/assettrack/service/notification/INotificationService.java b/backend/src/main/java/com/assettrack/service/notification/INotificationService.java index 4c0fc60..65b2116 100644 --- a/backend/src/main/java/com/assettrack/service/notification/INotificationService.java +++ b/backend/src/main/java/com/assettrack/service/notification/INotificationService.java @@ -7,6 +7,9 @@ import java.util.UUID; +/** + * Contract for in-app notification operations. + */ public interface INotificationService { Page getCurrentUserNotifications(Authentication authentication, Pageable pageable); diff --git a/backend/src/main/java/com/assettrack/service/notification/InventoryCheckPort.java b/backend/src/main/java/com/assettrack/service/notification/InventoryCheckPort.java index 6df6f11..97fe836 100644 --- a/backend/src/main/java/com/assettrack/service/notification/InventoryCheckPort.java +++ b/backend/src/main/java/com/assettrack/service/notification/InventoryCheckPort.java @@ -2,8 +2,15 @@ import java.util.List; +/** + * Port for reading inventory data required by alert jobs. + */ public interface InventoryCheckPort { List getLowStockItems(int threshold); - record LowStockItem(String itemName, int currentStock) {} + /** + * Low-stock inventory item projection. + */ + record LowStockItem(String itemName, int currentStock) { + } } diff --git a/backend/src/main/java/com/assettrack/service/notification/MockInventoryCheckPort.java b/backend/src/main/java/com/assettrack/service/notification/MockInventoryCheckPort.java index 99ad0d4..95aa301 100644 --- a/backend/src/main/java/com/assettrack/service/notification/MockInventoryCheckPort.java +++ b/backend/src/main/java/com/assettrack/service/notification/MockInventoryCheckPort.java @@ -4,6 +4,9 @@ import org.springframework.stereotype.Component; import java.util.List; +/** + * Non-production inventory port implementation that returns sample data. + */ @Component @Profile("!prod") public class MockInventoryCheckPort implements InventoryCheckPort { @@ -11,8 +14,7 @@ public class MockInventoryCheckPort implements InventoryCheckPort { public List getLowStockItems(int threshold) { // Mock data to return items with stock < threshold return List.of( - new LowStockItem("Laptop Charger", 2), - new LowStockItem("Wireless Mouse", 4) - ); + new LowStockItem("Laptop Charger", 2), + new LowStockItem("Wireless Mouse", 4)); } } diff --git a/backend/src/main/java/com/assettrack/service/notification/NotificationService.java b/backend/src/main/java/com/assettrack/service/notification/NotificationService.java index 5e4dcbf..1124536 100644 --- a/backend/src/main/java/com/assettrack/service/notification/NotificationService.java +++ b/backend/src/main/java/com/assettrack/service/notification/NotificationService.java @@ -13,9 +13,14 @@ import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import lombok.extern.slf4j.Slf4j; +/** + * Notification service for retrieving and updating user notifications. + */ @Service @RequiredArgsConstructor +@Slf4j public class NotificationService implements INotificationService { private final NotificationRepository notificationRepository; @@ -25,6 +30,7 @@ public class NotificationService implements INotificationService { @Transactional(readOnly = true) public Page getCurrentUserNotifications(Authentication authentication, Pageable pageable) { String recipient = getCurrentUserEmail(authentication); + log.debug("Loading notifications for recipient {}", recipient); return notificationRepository.findByRecipient(recipient, pageable) .map(this::toResponse); } @@ -32,12 +38,15 @@ public Page getCurrentUserNotifications(Authentication aut @Transactional public NotificationResponse markAsRead(java.util.UUID notificationId, Authentication authentication) { String recipient = getCurrentUserEmail(authentication); + log.debug("Marking notification {} as read for recipient {}", notificationId, recipient); Notification notification = notificationRepository.findByIdAndRecipient(notificationId, recipient) .orElseThrow(() -> new ResourceNotFoundException( "Notification not found with id: " + notificationId)); notification.setRead(true); - return toResponse(notificationRepository.save(notification)); + Notification saved = notificationRepository.save(notification); + log.info("Notification {} marked as read", notificationId); + return toResponse(saved); } private String getCurrentUserEmail(Authentication authentication) { diff --git a/backend/src/main/java/com/assettrack/service/user/IUserService.java b/backend/src/main/java/com/assettrack/service/user/IUserService.java index b14590c..9963d9d 100644 --- a/backend/src/main/java/com/assettrack/service/user/IUserService.java +++ b/backend/src/main/java/com/assettrack/service/user/IUserService.java @@ -9,6 +9,9 @@ import java.util.UUID; +/** + * Contract for user account and profile operations. + */ public interface IUserService { UserResponse getMyProfile(Authentication authentication); diff --git a/backend/src/main/java/com/assettrack/service/user/UserService.java b/backend/src/main/java/com/assettrack/service/user/UserService.java index 798da81..9e6b962 100644 --- a/backend/src/main/java/com/assettrack/service/user/UserService.java +++ b/backend/src/main/java/com/assettrack/service/user/UserService.java @@ -5,19 +5,29 @@ import com.assettrack.dto.user.UpdateEmailRequest; import com.assettrack.dto.user.UpdatePasswordRequest; import com.assettrack.dto.user.UserResponse; -import com.assettrack.exception.*; +import com.assettrack.exception.ActiveUserDeletionException; +import com.assettrack.exception.EmailAlreadyExistsException; +import com.assettrack.exception.InvalidPasswordException; +import com.assettrack.exception.InvalidRoleException; +import com.assettrack.exception.ResourceNotFoundException; +import com.assettrack.exception.SelfOperationException; import com.assettrack.mapper.user.UserMapper; import com.assettrack.repository.user.UserRepository; import com.assettrack.security.util.SecurityUtils; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.Authentication; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +/** + * User service for account and profile operations. + */ @Service @RequiredArgsConstructor +@Slf4j public class UserService implements IUserService { private final UserMapper userMapper; private final UserRepository userRepository; @@ -27,14 +37,16 @@ public class UserService implements IUserService { /** * Retrieves the profile of the currently authenticated user. * - * @param authentication the current user's authentication token - * @return the {@link UserResponse} DTO + * @param authentication current authentication token + * @return authenticated user's profile * @throws ResourceNotFoundException if the authenticated user no longer exists */ public UserResponse getMyProfile(Authentication authentication) { java.util.UUID currentId = securityUtils.getCurrentUserId(authentication); + log.debug("Fetching profile for user {}", currentId); User user = userRepository.findById(currentId) .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + currentId)); + log.info("Profile retrieved for user {} ({})", user.getEmail(), currentId); return userMapper.toResponse(user); } @@ -45,20 +57,17 @@ public UserResponse getMyProfile(Authentication authentication) { * @return a page of {@link UserResponse} DTOs */ public Page getAllUsers(Pageable pageable) { - Page users = userRepository.findAll(pageable); - return users.map(userMapper::toResponse); + return userRepository.findAll(pageable).map(userMapper::toResponse); } /** * Retrieves a paginated list of all inactive users. - * Used by admins to review deactivated accounts before permanent deletion. * * @param pageable pagination and sorting parameters - * @return a page of {@link UserResponse} DTOs representing inactive users + * @return a page of inactive users */ public Page getInactiveUsers(Pageable pageable) { - Page users = userRepository.findAllByIsActiveFalse(pageable); - return users.map(userMapper::toResponse); + return userRepository.findAllByIsActiveFalse(pageable).map(userMapper::toResponse); } /** @@ -69,14 +78,13 @@ public Page getInactiveUsers(Pageable pageable) { * @throws ResourceNotFoundException if no user exists with the given ID */ public UserResponse getUserById(java.util.UUID id) { - User user = userRepository.findById(id) + return userRepository.findById(id) + .map(userMapper::toResponse) .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id)); - return userMapper.toResponse(user); } /** * Updates the email address of the currently authenticated user. - * Requires password confirmation before applying the change. * * @param request contains the new email and current password for * verification @@ -89,23 +97,25 @@ public UserResponse getUserById(java.util.UUID id) { */ public UserResponse updateEmail(UpdateEmailRequest request, Authentication authentication) { java.util.UUID currentId = securityUtils.getCurrentUserId(authentication); + log.info("Updating email for user {}", currentId); User user = userRepository.findById(currentId) .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + currentId)); if (!passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) { + log.warn("Email update rejected due to invalid password for user {}", currentId); throw new InvalidPasswordException("Invalid password"); } if (userRepository.existsByEmail(request.getNewEmail())) { + log.warn("Email update rejected because email already exists: {}", request.getNewEmail()); throw new EmailAlreadyExistsException("Email already exists"); } user.setEmail(request.getNewEmail()); userRepository.save(user); + log.info("Email updated successfully for user {}", currentId); return userMapper.toResponse(user); } /** * Updates the password of the currently authenticated user. - * Requires the current password for verification before applying the change. - * The new password is encoded before storage. * * @param request contains the current password and the new password * @param authentication the current user's authentication token @@ -114,31 +124,32 @@ public UserResponse updateEmail(UpdateEmailRequest request, Authentication authe */ public void updatePassword(UpdatePasswordRequest request, Authentication authentication) { java.util.UUID currentId = securityUtils.getCurrentUserId(authentication); + log.info("Updating password for user {}", currentId); User user = userRepository.findById(currentId) .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + currentId)); if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPasswordHash())) { + log.warn("Password update rejected due to invalid current password for user {}", currentId); throw new InvalidPasswordException("Invalid password"); } user.setPasswordHash(passwordEncoder.encode(request.getNewPassword())); userRepository.save(user); + log.info("Password updated successfully for user {}", currentId); } /** * Updates the role of a target user. - * Admin cannot change their own role. * * @param id the target user's ID * @param role the new role as a string (case-insensitive) * @param authentication the current admin's authentication token * @return the updated {@link UserResponse} DTO * @throws ResourceNotFoundException if no user exists with the given ID - * @throws InvalidRoleException if the provided role string is not a valid - * {@link Role} + * @throws InvalidRoleException if the provided role string is not valid * @throws SelfOperationException if the admin attempts to change their own * role */ public UserResponse updateUserRole(java.util.UUID id, String role, Authentication authentication) { - + log.info("Updating role for user {} to {}", id, role); User user = userRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id)); java.util.UUID currentId = securityUtils.getCurrentUserId(authentication); @@ -146,23 +157,24 @@ public UserResponse updateUserRole(java.util.UUID id, String role, Authenticatio try { roleEnum = Role.valueOf(role.toUpperCase()); } catch (IllegalArgumentException e) { + log.warn("Invalid role '{}' requested for user {}", role, id); throw new InvalidRoleException("Invalid role"); } if (id.equals(currentId)) { + log.warn("Self role change rejected for admin user {}", currentId); throw new SelfOperationException("Admin cannot change his Role"); } user.setRole(roleEnum); userRepository.save(user); + log.info("Role updated successfully for user {}", id); return userMapper.toResponse(user); } /** * Updates the active status of a target user. - * Admin cannot change their own status. - * Deactivating a user prevents them from logging in. * * @param id the target user's ID - * @param active the new status — true to activate, false to deactivate + * @param active the new status * @param authentication the current admin's authentication token * @return the updated {@link UserResponse} DTO * @throws ResourceNotFoundException if no user exists with the given ID @@ -170,37 +182,38 @@ public UserResponse updateUserRole(java.util.UUID id, String role, Authenticatio * status */ public UserResponse updateUserStatus(java.util.UUID id, boolean active, Authentication authentication) { + log.info("Updating status for user {} to {}", id, active); User user = userRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id)); java.util.UUID currentId = securityUtils.getCurrentUserId(authentication); if (id.equals(currentId)) { + log.warn("Self status change rejected for admin user {}", currentId); throw new SelfOperationException("Admin cannot change their own status"); } user.setActive(active); userRepository.save(user); + log.info("Status updated successfully for user {}", id); return userMapper.toResponse(user); } /** - * Soft-deletes the currently authenticated user's own account - * by setting their status to inactive. - * The record is retained in the database for audit purposes. + * Soft-deletes the currently authenticated user's own account. * * @param authentication the current user's authentication token * @throws ResourceNotFoundException if the authenticated user no longer exists */ public void deleteSelf(Authentication authentication) { java.util.UUID currentId = securityUtils.getCurrentUserId(authentication); + log.info("Soft deleting own account for user {}", currentId); User user = userRepository.findById(currentId) .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + currentId)); user.setActive(false); userRepository.save(user); + log.info("Account deactivated for user {}", currentId); } /** * Permanently deletes a user from the database. - * Only inactive users can be hard-deleted. - * Admin cannot delete their own account via this method. * * @param id the target user's ID * @param authentication the current admin's authentication token @@ -210,16 +223,19 @@ public void deleteSelf(Authentication authentication) { * @throws ActiveUserDeletionException if the target user is still active */ public void deleteUser(java.util.UUID id, Authentication authentication) { + log.info("Deleting user {}", id); User user = userRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id)); java.util.UUID currentId = securityUtils.getCurrentUserId(authentication); if (id.equals(currentId)) { + log.warn("Self deletion rejected for admin user {}", currentId); throw new SelfOperationException("Admin cannot delete their own account"); } if (user.isActive()) { + log.warn("Deletion rejected because user {} is still active", id); throw new ActiveUserDeletionException("Cannot delete an active user. Deactivate first."); } userRepository.delete(user); + log.info("User {} deleted successfully", id); } - } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 1431bd2..4291cd5 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -16,8 +16,8 @@ spring: driver-class-name: org.postgresql.Driver jpa: hibernate: - ddl-auto: create - show-sql: true + ddl-auto: validate + show-sql: false config: import: "optional:file:.env[.properties]" mail: @@ -32,6 +32,8 @@ spring: starttls: enable: true +cors: + allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:5173,http://localhost:8080} mail: from: ${MAIL_FROM:no-reply@assettrack.local} diff --git a/backend/src/test/java/com/assettrack/repository/asset/AssetAllocationRepositoryTest.java b/backend/src/test/java/com/assettrack/repository/asset/AssetAllocationRepositoryTest.java index 93bdd9b..6722286 100644 --- a/backend/src/test/java/com/assettrack/repository/asset/AssetAllocationRepositoryTest.java +++ b/backend/src/test/java/com/assettrack/repository/asset/AssetAllocationRepositoryTest.java @@ -18,10 +18,12 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.test.context.ActiveProfiles; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest +@ActiveProfiles("test") class AssetAllocationRepositoryTest { @Autowired diff --git a/backend/src/test/java/com/assettrack/repository/asset/AssetRepositoryTest.java b/backend/src/test/java/com/assettrack/repository/asset/AssetRepositoryTest.java index 8350839..282ad5e 100644 --- a/backend/src/test/java/com/assettrack/repository/asset/AssetRepositoryTest.java +++ b/backend/src/test/java/com/assettrack/repository/asset/AssetRepositoryTest.java @@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.test.context.ActiveProfiles; import java.time.LocalDate; import java.util.Optional; @@ -15,6 +16,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; @DataJpaTest +@ActiveProfiles("test") class AssetRepositoryTest { @Autowired diff --git a/backend/src/test/java/com/assettrack/repository/asset/AssetSpecificationsTest.java b/backend/src/test/java/com/assettrack/repository/asset/AssetSpecificationsTest.java index 7f2a7f1..bc45eb1 100644 --- a/backend/src/test/java/com/assettrack/repository/asset/AssetSpecificationsTest.java +++ b/backend/src/test/java/com/assettrack/repository/asset/AssetSpecificationsTest.java @@ -10,12 +10,14 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.domain.Specification; +import org.springframework.test.context.ActiveProfiles; import java.time.LocalDate; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest +@ActiveProfiles("test") class AssetSpecificationsTest { @Autowired diff --git a/backend/src/test/java/com/assettrack/repository/asset/ConditionReportRepositoryTest.java b/backend/src/test/java/com/assettrack/repository/asset/ConditionReportRepositoryTest.java index ee71330..6eece00 100644 --- a/backend/src/test/java/com/assettrack/repository/asset/ConditionReportRepositoryTest.java +++ b/backend/src/test/java/com/assettrack/repository/asset/ConditionReportRepositoryTest.java @@ -16,6 +16,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.test.context.ActiveProfiles; import java.time.LocalDate; import java.time.LocalDateTime; @@ -24,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest +@ActiveProfiles("test") class ConditionReportRepositoryTest { @Autowired diff --git a/backend/src/test/java/com/assettrack/repository/notification/NotificationRepositoryTest.java b/backend/src/test/java/com/assettrack/repository/notification/NotificationRepositoryTest.java index 56a00d5..fbd377e 100644 --- a/backend/src/test/java/com/assettrack/repository/notification/NotificationRepositoryTest.java +++ b/backend/src/test/java/com/assettrack/repository/notification/NotificationRepositoryTest.java @@ -7,12 +7,14 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.test.context.ActiveProfiles; import java.time.LocalDateTime; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest +@ActiveProfiles("test") class NotificationRepositoryTest { @Autowired diff --git a/backend/src/test/java/com/assettrack/repository/user/UserRepositoryTest.java b/backend/src/test/java/com/assettrack/repository/user/UserRepositoryTest.java index 7f9f1f2..36ac94e 100644 --- a/backend/src/test/java/com/assettrack/repository/user/UserRepositoryTest.java +++ b/backend/src/test/java/com/assettrack/repository/user/UserRepositoryTest.java @@ -6,6 +6,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.test.context.ActiveProfiles; import java.util.Optional; @@ -13,6 +14,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; @DataJpaTest +@ActiveProfiles("test") class UserRepositoryTest { @Autowired diff --git a/backend/src/test/java/com/assettrack/security/SecurityFilterChainTest.java b/backend/src/test/java/com/assettrack/security/SecurityFilterChainTest.java index 298ddc6..b5f8570 100644 --- a/backend/src/test/java/com/assettrack/security/SecurityFilterChainTest.java +++ b/backend/src/test/java/com/assettrack/security/SecurityFilterChainTest.java @@ -2,17 +2,11 @@ import java.util.UUID; import com.assettrack.repository.user.UserRepository; -import com.assettrack.service.asset.AssetService; -import com.assettrack.service.auth.AuthService; -import com.assettrack.service.dashboard.DashboardService; -import com.assettrack.service.notification.NotificationService; -import com.assettrack.service.user.UserService; import com.assettrack.dto.user.UserResponse; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import java.util.Collections; -import java.util.List; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; diff --git a/backend/src/test/java/com/assettrack/service/asset/AssetServiceTest.java b/backend/src/test/java/com/assettrack/service/asset/AssetServiceTest.java index 8cf217f..570c9ce 100644 --- a/backend/src/test/java/com/assettrack/service/asset/AssetServiceTest.java +++ b/backend/src/test/java/com/assettrack/service/asset/AssetServiceTest.java @@ -7,9 +7,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.core.Authentication; -import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -27,7 +25,6 @@ import com.assettrack.domain.user.User; import com.assettrack.dto.asset.ConditionReportResponse; import com.assettrack.dto.asset.AssetSummaryResponse; -import com.assettrack.dto.user.UserResponse; import com.assettrack.exception.SelfOperationException; import com.assettrack.mapper.asset.AssetMapper; import com.assettrack.repository.asset.AssetAllocationRepository; diff --git a/frontend/src/api/services.js b/frontend/src/api/services.js index a4ef1a6..380d2f3 100644 --- a/frontend/src/api/services.js +++ b/frontend/src/api/services.js @@ -3,7 +3,7 @@ * AssetTrack — API service layer. * * Thin, domain-organised wrappers around the `apiClient` convenience methods. - * One service object per OpenAPI tag. Hooks import from here; components + * One service object per OpenAPI tag. Hooks import from here; components * never call the network directly. * * Every method returns a Promise that resolves to the typed response or @@ -35,6 +35,17 @@ export const authService = { * @returns {Promise} */ changePassword: (body) => patch('/auth/me/password', body), + + /** + * @param {{ email: string }} body + * @returns {Promise} + */ + updateEmail: (body) => put('/auth/me/email', body), + + /** + * @returns {Promise} + */ + deleteAccount: () => del('/auth/me'), }; export const userService = { @@ -44,27 +55,49 @@ export const userService = { */ list: (params) => get('/users', { params }), - /** @param {string} userId */ + /** + * @param {{ page?: number, size?: number }} params + * @returns {Promise} + */ + listInactive: (params) => get('/users/inactive', { params }), + + /** + * @param {string} userId + * @returns {Promise} + */ getById: (userId) => get(`/users/${userId}`), /** * @param {string} userId * @param {{ fullName?: string, active?: boolean }} body + * @returns {Promise} */ update: (userId, body) => patch(`/users/${userId}`, body), - /** @param {string} userId */ + /** + * @param {string} userId + * @param {{ active: boolean }} body + * @returns {Promise} + */ + updateStatus: (userId, body) => put(`/users/${userId}/status`, body), + + /** + * @param {string} userId + * @returns {Promise} + */ delete: (userId) => del(`/users/${userId}`), /** * @param {string} userId * @param {{ role: string }} body + * @returns {Promise} */ updateRole: (userId, body) => put(`/users/${userId}/role`, body), /** * @param {string} userId * @param {{ page?: number, size?: number }} params + * @returns {Promise} */ getAssets: (userId, params) => get(`/users/${userId}/assets`, { params }), }; @@ -83,16 +116,23 @@ export const assetService = { */ create: (body) => post('/assets', body), - /** @param {string} assetId */ + /** + * @param {string} assetId + * @returns {Promise} + */ getById: (assetId) => get(`/assets/${assetId}`), /** * @param {string} assetId - * @param {object} body - UpdateAssetRequest (partial) + * @param {object} body - UpdateAssetRequest + * @returns {Promise} */ update: (assetId, body) => patch(`/assets/${assetId}`, body), - /** @param {string} assetId */ + /** + * @param {string} assetId + * @returns {Promise} + */ delete: (assetId) => del(`/assets/${assetId}`), }; @@ -100,6 +140,7 @@ export const allocationService = { /** * @param {string} assetId * @param {{ page?: number, size?: number, active?: boolean }} params + * @returns {Promise} */ history: (assetId, params) => get(`/assets/${assetId}/allocations`, { params }), @@ -107,12 +148,14 @@ export const allocationService = { /** * @param {string} assetId * @param {{ assignedToUserId: string, notes?: string }} body + * @returns {Promise} */ allocate: (assetId, body) => post(`/assets/${assetId}/allocations`, body), /** * @param {string} assetId * @param {string} allocationId + * @returns {Promise} */ getById: (assetId, allocationId) => get(`/assets/${assetId}/allocations/${allocationId}`), @@ -121,47 +164,121 @@ export const allocationService = { * @param {string} assetId * @param {string} allocationId * @param {{ notes?: string }} [body] + * @returns {Promise} */ deallocate: (assetId, allocationId, body) => post(`/assets/${assetId}/allocations/${allocationId}/deallocate`, body), }; export const conditionReportService = { + /** + * @param {string} assetId + * @param {{ page?: number, size?: number }} params + * @returns {Promise} + */ list: (assetId, params) => get(`/assets/${assetId}/condition-reports`, { params }), + /** + * @param {string} assetId + * @param {object} body - CreateConditionReportRequest + * @returns {Promise} + */ create: (assetId, body) => post(`/assets/${assetId}/condition-reports`, body), + /** + * @param {string} assetId + * @param {string} reportId + * @returns {Promise} + */ getById: (assetId, reportId) => get(`/assets/${assetId}/condition-reports/${reportId}`), + /** + * @param {string} assetId + * @param {string} reportId + * @param {object} body - UpdateConditionReportRequest + * @returns {Promise} + */ update: (assetId, reportId, body) => patch(`/assets/${assetId}/condition-reports/${reportId}`, body), }; export const notificationService = { + /** + * @param {{ page?: number, size?: number, read?: boolean, type?: string }} params + * @returns {Promise} + */ list: (params) => get('/notifications', { params }), + + /** + * @param {string} notificationId + * @returns {Promise} + */ markRead: (notificationId) => patch(`/notifications/${notificationId}/read`), + + /** + * @returns {Promise} + */ markAllRead: () => post('/notifications/read-all'), + + /** + * @returns {Promise} + */ getPreferences: () => get('/notifications/preferences'), + + /** + * @param {object} body - NotificationPreferencesRequest + * @returns {Promise} + */ updatePreferences: (body) => put('/notifications/preferences', body), }; export const dashboardService = { + /** + * @returns {Promise} + */ inventory: () => get('/dashboard/inventory'), + + /** + * @param {{ page?: number, size?: number, withinDays?: number }} params + * @returns {Promise} + */ expiringWarranties: (params) => get('/dashboard/expiring-warranties', { params }), }; export const reportService = { + /** + * @param {{ page?: number, size?: number, userId?: string, assetType?: string }} params + * @returns {Promise} + */ allocations: (params) => get('/reports/allocations', { params }), + + /** + * @param {{ page?: number, size?: number, status?: string, severity?: string }} params + * @returns {Promise} + */ conditionReports: (params) => get('/reports/condition-reports', { params }), }; export const searchService = { + /** + * @param {object} params - Search filters and pagination + * @returns {Promise} + */ assets: (params) => get('/search/assets', { params }), + + /** + * @returns {Promise} + */ spareLaptop: () => get('/search/assets/spare-laptop'), + + /** + * @param {{ q: string, page?: number, size?: number }} params + * @returns {Promise} + */ users: (params) => get('/search/users', { params }), }; \ No newline at end of file diff --git a/frontend/src/lib/apiBaseUrl.js b/frontend/src/lib/apiBaseUrl.js index 5a427a0..8fbd5b6 100644 --- a/frontend/src/lib/apiBaseUrl.js +++ b/frontend/src/lib/apiBaseUrl.js @@ -1,3 +1,3 @@ -const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api'; +const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api/v1'; export default apiBaseUrl; diff --git a/frontend/src/tests/lib/axios.test.js b/frontend/src/tests/lib/axios.test.js index a71bb96..c210246 100644 --- a/frontend/src/tests/lib/axios.test.js +++ b/frontend/src/tests/lib/axios.test.js @@ -8,7 +8,7 @@ jest.mock('../../lib/navigation.js', () => ({ jest.mock('../../lib/apiBaseUrl.js', () => ({ __esModule: true, - default: 'http://localhost:8080/api', + default: 'http://localhost:8080/api/v1', })); jest.mock('../../store/useAuthStore.js', () => ({ @@ -41,7 +41,7 @@ afterEach(() => { describe('instance configuration', () => { test('baseURL points to the Spring Boot API root', () => { - expect(axiosInstance.defaults.baseURL).toBe('http://localhost:8080/api'); + expect(axiosInstance.defaults.baseURL).toBe('http://localhost:8080/api/v1'); }); test('timeout is 10 000 ms', () => {