diff --git a/pom.xml b/pom.xml index 6a8c65e..683deaa 100644 --- a/pom.xml +++ b/pom.xml @@ -51,20 +51,20 @@ runtime true - + io.jsonwebtoken jjwt-api 0.11.5 - + io.jsonwebtoken jjwt-impl 0.11.5 runtime - + io.jsonwebtoken jjwt-jackson @@ -93,13 +93,13 @@ org.springframework.boot spring-boot-starter-security - + org.springframework.security spring-security-test test - + org.springframework.security spring-security-oauth2-client diff --git a/src/main/java/com/libraryman_api/LibrarymanApiApplication.java b/src/main/java/com/libraryman_api/LibrarymanApiApplication.java index 89e3ed7..a2c9156 100644 --- a/src/main/java/com/libraryman_api/LibrarymanApiApplication.java +++ b/src/main/java/com/libraryman_api/LibrarymanApiApplication.java @@ -7,13 +7,13 @@ import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication -@EnableAsync +@EnableAsync(proxyTargetClass = true) @EnableScheduling -@EnableCaching +@EnableCaching(proxyTargetClass = true) public class LibrarymanApiApplication { public static void main(String[] args) { SpringApplication.run(LibrarymanApiApplication.class, args); } -} \ No newline at end of file +} diff --git a/src/main/java/com/libraryman_api/book/BookController.java b/src/main/java/com/libraryman_api/book/BookController.java index 3cd3af3..0e2660f 100644 --- a/src/main/java/com/libraryman_api/book/BookController.java +++ b/src/main/java/com/libraryman_api/book/BookController.java @@ -100,4 +100,4 @@ public BookDto updateBook(@PathVariable int id, @RequestBody BookDto bookDtoDeta public void deleteBook(@PathVariable int id) { bookService.deleteBook(id); } -} +} \ No newline at end of file diff --git a/src/main/java/com/libraryman_api/borrowing/BorrowingService.java b/src/main/java/com/libraryman_api/borrowing/BorrowingService.java index 6975e71..0b05b17 100644 --- a/src/main/java/com/libraryman_api/borrowing/BorrowingService.java +++ b/src/main/java/com/libraryman_api/borrowing/BorrowingService.java @@ -349,4 +349,4 @@ public BorrowingsDto EntityToDto(Borrowings borrowings) { borrowingsDto.setBook(bookService.EntityToDto(borrowings.getBook())); return borrowingsDto; } -} +} \ No newline at end of file diff --git a/src/main/java/com/libraryman_api/email/EmailService.java b/src/main/java/com/libraryman_api/email/EmailService.java index f5f80b3..c59e644 100644 --- a/src/main/java/com/libraryman_api/email/EmailService.java +++ b/src/main/java/com/libraryman_api/email/EmailService.java @@ -17,62 +17,88 @@ import org.springframework.stereotype.Service; /** - * Service class for sending emails asynchronously. - * This class handles the construction and sending of MIME email messages. + * Unified service class for sending emails asynchronously. + * Handles both general email sending and notifications. */ @Service public class EmailService implements EmailSender { private static final Logger LOGGER = LoggerFactory.getLogger(EmailService.class); + private final NotificationRepository notificationRepository; private final JavaMailSender mailSender; - @Value("${spring.mail.properties.domain_name}") + + @Value("${spring.mail.properties.domain_name}") // Domain name from application properties private String domainName; - /** - * Constructs a new {@code EmailService} with the specified {@link NotificationRepository} and {@link JavaMailSender}. - * - * @param notificationRepository the repository for managing notification entities - * @param mailSender the mail sender for sending email messages - */ public EmailService(NotificationRepository notificationRepository, JavaMailSender mailSender) { this.notificationRepository = notificationRepository; this.mailSender = mailSender; } /** - * Sends an email asynchronously to the specified recipient. - * If the email is successfully sent, the notification status is updated to SENT. - * If the email fails to send, the notification status is updated to FAILED and an exception is thrown. + * Sends a general email asynchronously. * - * @param to the recipient's email address - * @param email the content of the email to send - * @param subject the subject of the email - * @param notification the {@link Notifications} object representing the email notification + * @param to recipient's email + * @param body email content (HTML supported) + * @param subject subject of the email */ - @Override @Async - public void send(String to, String email, String subject, Notifications notification) { + public void sendEmail(String to, String body, String subject) { + sendEmail(to, body, subject, null); // Default 'from' to null + } + + /** + * Sends a general email asynchronously. + * + * @param to recipient's email + * @param body email content (HTML supported) + * @param subject subject of the email + * @param from sender's email address (overrides default if provided) + */ + @Async + public void sendEmail(String to, String body, String subject, String from) { try { MimeMessage mimeMessage = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, "utf-8"); - helper.setText(email, true); + + helper.setText(body, true); // true = enable HTML content helper.setTo(to); helper.setSubject(subject); - helper.setFrom(domainName); + helper.setFrom(from != null ? from : domainName); // Use provided sender or default domain + mailSender.send(mimeMessage); + } catch (MessagingException e) { + LOGGER.error("Failed to send email", e); + throw new IllegalStateException("Failed to send email", e); + } + } + + /** + * Sends a notification email and updates notification status. + * + * @param to recipient's email + * @param email email content + * @param subject subject of the email + * @param notification notification entity to update status + */ + @Override + @Async + public void send(String to, String email, String subject, Notifications notification) { + try { + sendEmail(to, email, subject); // Reuse sendEmail method for notifications // Update notification status to SENT notification.setNotificationStatus(NotificationStatus.SENT); notificationRepository.save(notification); - } catch (MessagingException e) { - LOGGER.error("Failed to send email", e); + } catch (Exception e) { + LOGGER.error("Failed to send notification email", e); // Update notification status to FAILED notification.setNotificationStatus(NotificationStatus.FAILED); notificationRepository.save(notification); - throw new IllegalStateException("Failed to send email", e); + throw new IllegalStateException("Failed to send notification email", e); } } } diff --git a/src/main/java/com/libraryman_api/member/MemberService.java b/src/main/java/com/libraryman_api/member/MemberService.java index 553d650..518b7f1 100644 --- a/src/main/java/com/libraryman_api/member/MemberService.java +++ b/src/main/java/com/libraryman_api/member/MemberService.java @@ -225,4 +225,4 @@ public MembersDto EntityToDto(Members members) { membersDto.setMembershipDate(members.getMembershipDate()); return membersDto; } -} +} \ No newline at end of file diff --git a/src/main/java/com/libraryman_api/newsletter/NewsletterController.java b/src/main/java/com/libraryman_api/newsletter/NewsletterController.java index fe038fe..c1cbc8b 100644 --- a/src/main/java/com/libraryman_api/newsletter/NewsletterController.java +++ b/src/main/java/com/libraryman_api/newsletter/NewsletterController.java @@ -1,44 +1,71 @@ package com.libraryman_api.newsletter; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.Map; - @RestController @RequestMapping("/api/newsletter") public class NewsletterController { - @Autowired - private NewsletterService newsletterService; + private final NewsletterService newsletterService; + + public NewsletterController(NewsletterService newsletterService) { + this.newsletterService = newsletterService; + } - // Subscribe endpoint + // Subscribe Endpoint @PostMapping("/subscribe") - public ResponseEntity subscribe(@RequestBody Map requestBody) { - String email = requestBody.get("email"); + public ResponseEntity subscribe(@RequestParam String email) { + try { + String result = newsletterService.subscribe(email); - // Call the service to handle subscription - String response = newsletterService.subscribe(email); + switch (result) { + case "Invalid email format.": + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result); // 400 Bad Request - // Return response from the service - if (response.equals("Invalid email format.") || response.equals("Email is already subscribed.")) { - return ResponseEntity.badRequest().body(response); - } + case "Email is already subscribed.": + return ResponseEntity.status(HttpStatus.CONFLICT).body(result); // 409 Conflict - return ResponseEntity.ok(response); - } + case "You have successfully subscribed!": + return ResponseEntity.status(HttpStatus.CREATED).body(result); // 201 Created - // Unsubscribe endpoint using token - @GetMapping("/unsubscribe/{token}") - public ResponseEntity unsubscribe(@PathVariable String token) { - String response = newsletterService.unsubscribe(token); + case "You have successfully re-subscribed!": + return ResponseEntity.status(HttpStatus.OK).body(result); // 200 OK - // Check if the response indicates an error - if (response.equals("Invalid or expired token.") || response.equals("You are already unsubscribed.")) { - return ResponseEntity.badRequest().body(response); + default: + return ResponseEntity.status(HttpStatus.OK).body(result); // Default 200 OK + } + } catch (Exception e) { + // Handle unexpected errors + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("An error occurred while processing your subscription."); } + } - return ResponseEntity.ok(response); + // Unsubscribe Endpoint + @GetMapping("/unsubscribe") + public ResponseEntity unsubscribe(@RequestParam String token) { + try { + String result = newsletterService.unsubscribe(token); + + switch (result) { + case "Invalid or expired token.": + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(result); // 404 Not Found + + case "You are already unsubscribed.": + return ResponseEntity.status(HttpStatus.CONFLICT).body(result); // 409 Conflict + + case "You have successfully unsubscribed!": + return ResponseEntity.status(HttpStatus.OK).body(result); // 200 OK + + default: + return ResponseEntity.status(HttpStatus.OK).body(result); // Default 200 OK + } + } catch (Exception e) { + // Handle unexpected errors + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("An error occurred while processing your unsubscription."); + } } } diff --git a/src/main/java/com/libraryman_api/newsletter/NewsletterService.java b/src/main/java/com/libraryman_api/newsletter/NewsletterService.java index 034fd78..5c12cd1 100644 --- a/src/main/java/com/libraryman_api/newsletter/NewsletterService.java +++ b/src/main/java/com/libraryman_api/newsletter/NewsletterService.java @@ -1,74 +1,76 @@ package com.libraryman_api.newsletter; +import com.libraryman_api.email.EmailService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; - import java.util.Optional; -import java.util.regex.Matcher; import java.util.regex.Pattern; @Service public class NewsletterService { + private final NewsletterSubscriberRepository subscriberRepository; + private final EmailService emailService; + @Autowired - private NewsletterSubscriberRepository subscriberRepository; + public NewsletterService(NewsletterSubscriberRepository subscriberRepository, EmailService emailService) { + this.subscriberRepository = subscriberRepository; + this.emailService = emailService; + } - // Subscribe user after validating email public String subscribe(String email) { - if (!isValidEmail(email)) { - return "Invalid email format."; - } + if (!isValidEmail(email)) return "Invalid email format."; - // Check if the email already exists Optional optionalSubscriber = subscriberRepository.findByEmail(email); - if (optionalSubscriber.isPresent()) { NewsletterSubscriber subscriber = optionalSubscriber.get(); - - // If the subscriber is inactive, reactivate them if (!subscriber.isActive()) { - subscriber.setActive(true); // Reactivate the subscriber - subscriber.regenerateToken(); // Generate a new token - subscriberRepository.save(subscriber); // Save the updated subscriber + subscriber.setActive(true); + subscriber.regenerateToken(); + subscriberRepository.save(subscriber); + sendSubscriptionEmail(email, subscriber.getUnsubscribeToken()); return "You have successfully re-subscribed!"; - } else { - return "Email is already subscribed."; } + return "Email is already subscribed."; } - // Save new subscriber if not present - NewsletterSubscriber subscriber = new NewsletterSubscriber(email); - subscriberRepository.save(subscriber); + NewsletterSubscriber newSubscriber = new NewsletterSubscriber(email); + subscriberRepository.save(newSubscriber); + sendSubscriptionEmail(email, newSubscriber.getUnsubscribeToken()); return "You have successfully subscribed!"; } - // Unsubscribe user using the token public String unsubscribe(String token) { Optional optionalSubscriber = subscriberRepository.findByUnsubscribeToken(token); - - if (optionalSubscriber.isEmpty()) { - return "Invalid or expired token."; - } + if (optionalSubscriber.isEmpty()) return "Invalid or expired token."; NewsletterSubscriber subscriber = optionalSubscriber.get(); + if (!subscriber.isActive()) return "You are already unsubscribed."; - if (!subscriber.isActive()) { - return "You are already unsubscribed."; - } - - subscriber.setActive(false); // Set active to false - subscriberRepository.save(subscriber); // Save the updated subscriber + subscriber.setActive(false); + subscriberRepository.save(subscriber); + sendUnsubscribeEmail(subscriber.getEmail()); return "You have successfully unsubscribed!"; } - // Email validation logic private boolean isValidEmail(String email) { - if (email == null || email.trim().isEmpty()) { - return false; - } String emailRegex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"; - Pattern pattern = Pattern.compile(emailRegex); - Matcher matcher = pattern.matcher(email); - return matcher.matches(); + return Pattern.compile(emailRegex).matcher(email).matches(); + } + + private void sendSubscriptionEmail(String email, String token) { + String subject = "Welcome to Our Newsletter!"; + String body = "Thank you for subscribing! " + + "To unsubscribe, click the link:\n" + + "http://localhost:8080/api/newsletter/unsubscribe?token=" + token; + + emailService.sendEmail(email, body, subject); // No need to change this line + } + + private void sendUnsubscribeEmail(String email) { + String subject = "You have been unsubscribed"; + String body = "You have successfully unsubscribed. If this was a mistake, you can re-subscribe."; + + emailService.sendEmail(email, body, subject); // No need to change this line } } diff --git a/src/main/java/com/libraryman_api/newsletter/NewsletterSubscriber.java b/src/main/java/com/libraryman_api/newsletter/NewsletterSubscriber.java index aa099c0..e6c73a2 100644 --- a/src/main/java/com/libraryman_api/newsletter/NewsletterSubscriber.java +++ b/src/main/java/com/libraryman_api/newsletter/NewsletterSubscriber.java @@ -1,7 +1,6 @@ package com.libraryman_api.newsletter; import jakarta.persistence.*; - import java.util.UUID; @Entity @@ -16,53 +15,28 @@ public class NewsletterSubscriber { private String email; @Column(nullable = false) - private boolean active = true; // Manage subscription status + private boolean active = true; @Column(name = "unsubscribe_token", nullable = false, unique = true) - private String unsubscribeToken; // Token for unsubscribing + private String unsubscribeToken; - // Default constructor that initializes the token + // Default constructor initializing unsubscribe token public NewsletterSubscriber() { - this.unsubscribeToken = UUID.randomUUID().toString(); // Generate token by default + this.unsubscribeToken = UUID.randomUUID().toString(); } - // Constructor to initialize with email + // Constructor initializing with email public NewsletterSubscriber(String email) { this(); this.email = email; } - // Getters and setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public boolean isActive() { - return active; - } - - public void setActive(boolean active) { - this.active = active; - } - - public String getUnsubscribeToken() { - return unsubscribeToken; - } - - // Method to regenerate a new token - public void regenerateToken() { - this.unsubscribeToken = UUID.randomUUID().toString(); - } + // Getters and Setters + public Long getId() { return id; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public boolean isActive() { return active; } + public void setActive(boolean active) { this.active = active; } + public String getUnsubscribeToken() { return unsubscribeToken; } + public void regenerateToken() { this.unsubscribeToken = UUID.randomUUID().toString(); } } diff --git a/src/main/java/com/libraryman_api/newsletter/NewsletterSubscriberRepository.java b/src/main/java/com/libraryman_api/newsletter/NewsletterSubscriberRepository.java index 1e6d215..2604620 100644 --- a/src/main/java/com/libraryman_api/newsletter/NewsletterSubscriberRepository.java +++ b/src/main/java/com/libraryman_api/newsletter/NewsletterSubscriberRepository.java @@ -1,14 +1,9 @@ package com.libraryman_api.newsletter; import org.springframework.data.jpa.repository.JpaRepository; - import java.util.Optional; public interface NewsletterSubscriberRepository extends JpaRepository { - - // Find a subscriber by email Optional findByEmail(String email); - - // Find a subscriber by unsubscribe token Optional findByUnsubscribeToken(String unsubscribeToken); } diff --git a/src/main/java/com/libraryman_api/security/config/WebConfiguration.java b/src/main/java/com/libraryman_api/security/config/WebConfiguration.java index 61b4002..413db45 100644 --- a/src/main/java/com/libraryman_api/security/config/WebConfiguration.java +++ b/src/main/java/com/libraryman_api/security/config/WebConfiguration.java @@ -86,4 +86,4 @@ public CorsFilter corsFilter() { return new CorsFilter(corsConfigurationSource()); } -} +} \ No newline at end of file diff --git a/src/main/java/com/libraryman_api/security/services/SignupService.java b/src/main/java/com/libraryman_api/security/services/SignupService.java index cb7bb64..4d6b0e8 100644 --- a/src/main/java/com/libraryman_api/security/services/SignupService.java +++ b/src/main/java/com/libraryman_api/security/services/SignupService.java @@ -84,4 +84,4 @@ public void signupLibrarian(Members members) { new_members.setUsername(members.getUsername()); memberRepository.save(new_members); } -} +} \ No newline at end of file diff --git a/src/main/resources/application-development.properties b/src/main/resources/application-development.properties index 3f7d168..7175765 100644 --- a/src/main/resources/application-development.properties +++ b/src/main/resources/application-development.properties @@ -42,10 +42,8 @@ spring.mail.properties.mail.smtp.auth=Add_Your_Mail_Service_SMTP spring.mail.properties.mail.starttls.enable=Add_Your_Mail_Service_Start_TLS spring.mail.properties.domain_name=Add_Your_Mail_Service_Domain_Name - - # --- Oauth 2.0 Configurations --- spring.security.oauth2.client.registration.google.client-name=google spring.security.oauth2.client.registration.google.client-id=ADD_YOUR_CLIENT_ID spring.security.oauth2.client.registration.google.client-secret=ADD_YOUR_SECRET_KEY -spring.security.oauth2.client.registration.google.scope=email,profile +spring.security.oauth2.client.registration.google.scope=email,profile \ No newline at end of file