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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.requestMatchers(HttpMethod.GET, "/api/v1/contacts/options").permitAll()

// 🔒 Admin endpoint
.requestMatchers(HttpMethod.GET, "/api/v1/contacts").authenticated()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/v1/contacts").hasRole("ADMIN")

// Everything else
.anyRequest().permitAll()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.vimaltech.contactapi.controller.admin;

import com.vimaltech.contactapi.dto.admin.EmailLogResponse;
import com.vimaltech.contactapi.dto.admin.PagedResponse;
import com.vimaltech.contactapi.dto.admin.RetryOperationResponse;
import com.vimaltech.contactapi.dto.admin.RetryStatisticsResponse;
import com.vimaltech.contactapi.enums.EmailStatus;
import com.vimaltech.contactapi.service.admin.EmailOperationsService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/admin/email-retries")
@RequiredArgsConstructor
public class EmailRetryAdminController {

private final EmailOperationsService emailOperationsService;

@GetMapping("/stats")
public ResponseEntity<RetryStatisticsResponse> getStatistics() {

return ResponseEntity.ok(
emailOperationsService.getStatistics()
);
}

@GetMapping
public ResponseEntity<PagedResponse<EmailLogResponse>> getEmailLogs(

@RequestParam(required = false)
EmailStatus status,

@RequestParam(defaultValue = "0")
int page,

@RequestParam(defaultValue = "20")
int size
) {

Pageable pageable = PageRequest.of(page, size);

Page<EmailLogResponse> emailPage =
emailOperationsService.getEmailLogs(
status,
pageable
);

PagedResponse<EmailLogResponse> response =
new PagedResponse<>(
emailPage.getContent(),
emailPage.getNumber(),
emailPage.getSize(),
emailPage.getTotalElements(),
emailPage.getTotalPages(),
emailPage.isFirst(),
emailPage.isLast()
);

return ResponseEntity.ok(response);
}

@GetMapping("/{id}")
public ResponseEntity<EmailLogResponse> getEmailLog(
@PathVariable Long id
) {

return ResponseEntity.ok(
emailOperationsService.getEmailLog(id)
);
}

@PostMapping("/{id}/retry")
public ResponseEntity<RetryOperationResponse> retryEmail(
@PathVariable Long id
) {

return ResponseEntity.ok(
emailOperationsService.retryEmail(id)
);
}

@PostMapping("/retry-all")
public ResponseEntity<RetryOperationResponse> retryAllFailedEmails() {

return ResponseEntity.ok(
emailOperationsService.retryAllFailedEmails()
);
}

@PostMapping("/{id}/reset")
public ResponseEntity<RetryOperationResponse> resetPermanentFailure(
@PathVariable Long id
) {

return ResponseEntity.ok(
emailOperationsService.resetPermanentFailure(id)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.vimaltech.contactapi.dto.admin;

import com.vimaltech.contactapi.enums.EmailStatus;

import java.time.LocalDateTime;

public record EmailLogResponse(

Long id,

String toEmail,

String subject,

EmailStatus status,

int retryCount,

String lastError,

LocalDateTime createdAt,

LocalDateTime lastAttemptAt
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.vimaltech.contactapi.dto.admin;

import java.util.List;

public record PagedResponse<T>(

List<T> content,

int page,

int size,

long totalElements,

int totalPages,

boolean first,

boolean last
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.vimaltech.contactapi.dto.admin;

import java.time.LocalDateTime;

public record RetryOperationResponse(

boolean success,

String message,

Long emailId,

LocalDateTime timestamp
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.vimaltech.contactapi.dto.admin;

public record RetryStatisticsResponse(

long pending,

long inProgress,

long sent,

long failed,

long failedPermanent
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.vimaltech.contactapi.exception;

public class EmailNotFoundException extends RuntimeException {

public EmailNotFoundException(Long id) {
super("Email log not found with id: " + id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.LocalDateTime;

@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private static final Logger log =
LoggerFactory.getLogger(GlobalExceptionHandler.class);

// ✅ 1. Bean validation errors (@Valid)
@ExceptionHandler(MethodArgumentNotValidException.class)
Expand All @@ -39,22 +41,25 @@ public ResponseEntity<ApiResponse> handleValidationException(
return ResponseEntity.badRequest().body(response);
}

// ✅ 2. JSON parsing / ENUM errors (UPDATED - precise handling)
// ✅ 2. JSON parsing / ENUM errors
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiResponse> handleJsonParseError(HttpMessageNotReadableException ex) {
public ResponseEntity<ApiResponse> handleJsonParseError(
HttpMessageNotReadableException ex) {

log.warn("Invalid request format or enum value: {}", ex.getMessage());
log.warn(
"Invalid request format or enum value: {}",
ex.getMessage()
);

String message = "Invalid request format";

// 🔥 Extract root cause (THIS is what you asked about)
Throwable root = ex.getCause();

if (root instanceof IllegalArgumentException &&
root.getMessage() != null &&
root.getMessage().contains("Invalid subject value")) {
if (root instanceof IllegalArgumentException
&& root.getMessage() != null
&& root.getMessage().contains("Invalid subject value")) {

message = root.getMessage(); // use exact enum error
message = root.getMessage();
}

ApiResponse response = new ApiResponse(
Expand All @@ -66,9 +71,27 @@ public ResponseEntity<ApiResponse> handleJsonParseError(HttpMessageNotReadableEx
return ResponseEntity.badRequest().body(response);
}

// ✅ 3. Catch-all (fallback)
// ✅ 3. Email log not found
@ExceptionHandler(EmailNotFoundException.class)
public ResponseEntity<ApiResponse> handleEmailNotFoundException(
EmailNotFoundException ex) {

log.warn("Email log not found: {}", ex.getMessage());

ApiResponse response = new ApiResponse(
false,
ex.getMessage(),
LocalDateTime.now()
);

return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(response);
}

// ✅ 4. Catch-all (fallback)
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse> handleGenericException(Exception ex) {
public ResponseEntity<ApiResponse> handleGenericException(
Exception ex) {

log.error("Unhandled exception occurred", ex);

Expand All @@ -78,6 +101,8 @@ public ResponseEntity<ApiResponse> handleGenericException(Exception ex) {
LocalDateTime.now()
);

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
return ResponseEntity.status(
HttpStatus.INTERNAL_SERVER_ERROR
).body(response);
}
}
26 changes: 26 additions & 0 deletions src/main/java/com/vimaltech/contactapi/mapper/EmailLogMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.vimaltech.contactapi.mapper;

import com.vimaltech.contactapi.dto.admin.EmailLogResponse;
import com.vimaltech.contactapi.entity.EmailLog;

public final class EmailLogMapper {

private EmailLogMapper() {
}

public static EmailLogResponse toResponse(
EmailLog emailLog
) {

return new EmailLogResponse(
emailLog.getId(),
emailLog.getToEmail(),
emailLog.getSubject(),
emailLog.getStatus(),
emailLog.getRetryCount(),
emailLog.getLastError(),
emailLog.getCreatedAt(),
emailLog.getLastAttemptAt()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.vimaltech.contactapi.entity.EmailLog;
import com.vimaltech.contactapi.enums.EmailStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

Expand All @@ -14,4 +16,17 @@ public interface EmailLogRepository
List<EmailLog> findTop10ByStatusOrderByCreatedAtAsc(
EmailStatus status
);

Page<EmailLog> findByStatus(
EmailStatus status,
Pageable pageable
);

List<EmailLog> findByStatus(
EmailStatus status
);

long countByStatus(
EmailStatus status
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.vimaltech.contactapi.service.admin;

import com.vimaltech.contactapi.dto.admin.EmailLogResponse;
import com.vimaltech.contactapi.dto.admin.RetryOperationResponse;
import com.vimaltech.contactapi.dto.admin.RetryStatisticsResponse;
import com.vimaltech.contactapi.enums.EmailStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface EmailOperationsService {

RetryStatisticsResponse getStatistics();

Page<EmailLogResponse> getEmailLogs(
EmailStatus status,
Pageable pageable
);

EmailLogResponse getEmailLog(Long id);

RetryOperationResponse retryEmail(Long id);

RetryOperationResponse retryAllFailedEmails();

RetryOperationResponse resetPermanentFailure(Long id);
}
Loading
Loading