diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/controller/EmailController.java b/booklore-api/src/main/java/com/adityachandel/booklore/controller/EmailController.java deleted file mode 100644 index eef212018..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/controller/EmailController.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.adityachandel.booklore.controller; - -import com.adityachandel.booklore.model.dto.request.SendBookByEmailRequest; -import com.adityachandel.booklore.service.email.EmailService; -import lombok.AllArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -@Deprecated -@AllArgsConstructor -@RestController -@RequestMapping("/api/v1/emails") -public class EmailController { - - private final EmailService emailService; - - @PreAuthorize("@securityUtil.canEmailBook() or @securityUtil.isAdmin()") - @PostMapping("/send-book") - public ResponseEntity sendEmail(@Validated @RequestBody SendBookByEmailRequest request) { - emailService.emailBook(request); - return ResponseEntity.noContent().build(); - } - - @PreAuthorize("@securityUtil.canEmailBook() or @securityUtil.isAdmin()") - @PostMapping("/send-book/{bookId}") - public ResponseEntity emailBookQuick(@PathVariable Long bookId) { - emailService.emailBookQuick(bookId); - return ResponseEntity.noContent().build(); - } -} \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/controller/EmailProviderController.java b/booklore-api/src/main/java/com/adityachandel/booklore/controller/EmailProviderController.java deleted file mode 100644 index ff384de9c..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/controller/EmailProviderController.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.adityachandel.booklore.controller; - -import com.adityachandel.booklore.model.dto.EmailProvider; -import com.adityachandel.booklore.model.dto.request.CreateEmailProviderRequest; -import com.adityachandel.booklore.service.email.EmailProviderService; -import lombok.AllArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@Deprecated -@AllArgsConstructor -@RestController -@RequestMapping("/api/v1/email/providers") -public class EmailProviderController { - - private final EmailProviderService emailProviderService; - - @PreAuthorize("@securityUtil.isAdmin() or @securityUtil.canEmailBook()") - @GetMapping - public ResponseEntity> getEmailProviders() { - return ResponseEntity.ok(emailProviderService.getEmailProviders()); - } - - @PreAuthorize("@securityUtil.isAdmin() or @securityUtil.canEmailBook()") - @GetMapping("/{id}") - public ResponseEntity getEmailProvider(@PathVariable Long id) { - return ResponseEntity.ok(emailProviderService.getEmailProvider(id)); - } - - @PreAuthorize("@securityUtil.isAdmin()") - @PostMapping - public ResponseEntity createEmailProvider(@RequestBody CreateEmailProviderRequest createEmailProviderRequest) { - return ResponseEntity.ok(emailProviderService.createEmailProvider(createEmailProviderRequest)); - } - - @PreAuthorize("@securityUtil.isAdmin()") - @PutMapping("/{id}") - public ResponseEntity updateEmailProvider(@PathVariable Long id, @RequestBody CreateEmailProviderRequest updateRequest) { - return ResponseEntity.ok(emailProviderService.updateEmailProvider(id, updateRequest)); - } - - @PreAuthorize("@securityUtil.isAdmin()") - @PatchMapping("/{id}/set-default") - public ResponseEntity setDefaultEmailProvider(@PathVariable Long id) { - emailProviderService.setDefaultEmailProvider(id); - return ResponseEntity.noContent().build(); - } - - @PreAuthorize("@securityUtil.isAdmin()") - @DeleteMapping("/{id}") - public ResponseEntity deleteEmailProvider(@PathVariable Long id) { - emailProviderService.deleteEmailProvider(id); - return ResponseEntity.noContent().build(); - } -} \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/controller/EmailRecipientController.java b/booklore-api/src/main/java/com/adityachandel/booklore/controller/EmailRecipientController.java deleted file mode 100644 index efe412954..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/controller/EmailRecipientController.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.adityachandel.booklore.controller; - -import com.adityachandel.booklore.model.dto.EmailRecipient; -import com.adityachandel.booklore.model.dto.request.CreateEmailRecipientRequest; -import com.adityachandel.booklore.service.email.EmailRecipientService; -import lombok.AllArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@Deprecated -@AllArgsConstructor -@RestController -@RequestMapping("/api/v1/email/recipients") -public class EmailRecipientController { - - private final EmailRecipientService emailRecipientService; - - @PreAuthorize("@securityUtil.isAdmin() or @securityUtil.canEmailBook()") - @GetMapping - public ResponseEntity> getEmailRecipients() { - return ResponseEntity.ok(emailRecipientService.getEmailRecipients()); - } - - @PreAuthorize("@securityUtil.isAdmin() or @securityUtil.canEmailBook()") - @GetMapping("/{id}") - public ResponseEntity getEmailRecipient(@PathVariable Long id) { - return ResponseEntity.ok(emailRecipientService.getEmailRecipient(id)); - } - - @PreAuthorize("@securityUtil.isAdmin()") - @PostMapping - public ResponseEntity createEmailRecipient(@RequestBody CreateEmailRecipientRequest createEmailRecipientRequest) { - return ResponseEntity.ok(emailRecipientService.createEmailRecipient(createEmailRecipientRequest)); - } - - @PreAuthorize("@securityUtil.isAdmin()") - @PutMapping("/{id}") - public ResponseEntity updateEmailRecipient(@PathVariable Long id, @RequestBody CreateEmailRecipientRequest updateRequest) { - return ResponseEntity.ok(emailRecipientService.updateEmailRecipient(id, updateRequest)); - } - - @PreAuthorize("@securityUtil.isAdmin()") - @PatchMapping("/{id}/set-default") - public ResponseEntity setDefaultEmailRecipient(@PathVariable Long id) { - emailRecipientService.setDefaultRecipient(id); - return ResponseEntity.noContent().build(); - } - - @PreAuthorize("@securityUtil.isAdmin()") - @DeleteMapping("/{id}") - public ResponseEntity deleteEmailRecipient(@PathVariable Long id) { - emailRecipientService.deleteEmailRecipient(id); - return ResponseEntity.noContent().build(); - } -} \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/controller/SendEmailV2Controller.java b/booklore-api/src/main/java/com/adityachandel/booklore/controller/SendEmailV2Controller.java index 5cf7c3ab8..7d8e54ce0 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/controller/SendEmailV2Controller.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/controller/SendEmailV2Controller.java @@ -1,7 +1,6 @@ package com.adityachandel.booklore.controller; import com.adityachandel.booklore.model.dto.request.SendBookByEmailRequest; -import com.adityachandel.booklore.service.email.EmailService; import com.adityachandel.booklore.service.email.SendEmailV2Service; import lombok.AllArgsConstructor; import org.springframework.http.ResponseEntity; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/mapper/EmailProviderMapper.java b/booklore-api/src/main/java/com/adityachandel/booklore/mapper/EmailProviderMapper.java deleted file mode 100644 index 059fc3119..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/mapper/EmailProviderMapper.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.adityachandel.booklore.mapper; - -import com.adityachandel.booklore.model.dto.EmailProvider; -import com.adityachandel.booklore.model.dto.request.CreateEmailProviderRequest; -import com.adityachandel.booklore.model.entity.EmailProviderEntity; -import org.mapstruct.Mapper; -import org.mapstruct.MappingTarget; - -@Mapper(componentModel = "spring") -public interface EmailProviderMapper { - - EmailProvider toDTO(EmailProviderEntity emailProviderEntity); - EmailProviderEntity toEntity(EmailProvider emailProvider); - EmailProviderEntity toEntity(CreateEmailProviderRequest createRequest); - void updateEntityFromRequest(CreateEmailProviderRequest request, @MappingTarget EmailProviderEntity entity); -} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/mapper/EmailProviderV2Mapper.java b/booklore-api/src/main/java/com/adityachandel/booklore/mapper/EmailProviderV2Mapper.java index 364eb3ece..b4f2170e6 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/mapper/EmailProviderV2Mapper.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/mapper/EmailProviderV2Mapper.java @@ -3,14 +3,24 @@ package com.adityachandel.booklore.mapper; import com.adityachandel.booklore.model.dto.EmailProviderV2; import com.adityachandel.booklore.model.dto.request.CreateEmailProviderRequest; import com.adityachandel.booklore.model.entity.EmailProviderV2Entity; -import org.mapstruct.Mapper; -import org.mapstruct.MappingTarget; +import org.mapstruct.*; @Mapper(componentModel = "spring") public interface EmailProviderV2Mapper { - EmailProviderV2 toDTO(EmailProviderV2Entity entity); - EmailProviderV2Entity toEntity(EmailProviderV2 emailProvider); - EmailProviderV2Entity toEntity(CreateEmailProviderRequest createRequest); + @Mapping(target = "defaultProvider", expression = "java(entity.getId() != null && entity.getId().equals(defaultProviderId))") + EmailProviderV2 toDTO(EmailProviderV2Entity entity, @Context Long defaultProviderId); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "userId", ignore = true) + @Mapping(target = "defaultProvider", ignore = true) + @Mapping(target = "shared", ignore = true) + EmailProviderV2Entity toEntity(CreateEmailProviderRequest request); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "userId", ignore = true) + @Mapping(target = "defaultProvider", ignore = true) + @Mapping(target = "shared", ignore = true) + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) void updateEntityFromRequest(CreateEmailProviderRequest request, @MappingTarget EmailProviderV2Entity entity); } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/mapper/EmailRecipientMapper.java b/booklore-api/src/main/java/com/adityachandel/booklore/mapper/EmailRecipientMapper.java deleted file mode 100644 index 8708b13d3..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/mapper/EmailRecipientMapper.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.adityachandel.booklore.mapper; - -import com.adityachandel.booklore.model.dto.EmailRecipient; -import com.adityachandel.booklore.model.dto.request.CreateEmailRecipientRequest; -import com.adityachandel.booklore.model.entity.EmailRecipientEntity; -import org.mapstruct.Mapper; -import org.mapstruct.MappingTarget; - -@Mapper(componentModel = "spring") -public interface EmailRecipientMapper { - - EmailRecipient toDTO(EmailRecipientEntity emailRecipientEntity); - - EmailRecipientEntity toEntity(EmailRecipient emailRecipient); - - EmailRecipientEntity toEntity(CreateEmailRecipientRequest createRequest); - - void updateEntityFromRequest(CreateEmailRecipientRequest request, @MappingTarget EmailRecipientEntity entity); -} \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EmailProvider.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EmailProvider.java deleted file mode 100644 index 8dd3cbf28..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EmailProvider.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.adityachandel.booklore.model.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Deprecated -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class EmailProvider { - private Long id; - private String name; - private String host; - private Integer port; - private String username; - private String password; - private String fromAddress; - private Boolean auth; - private Boolean startTls; - private Boolean defaultProvider; -} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EmailProviderV2.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EmailProviderV2.java index 3cf685ced..67e30ffb5 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EmailProviderV2.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EmailProviderV2.java @@ -16,7 +16,6 @@ public class EmailProviderV2 { private String host; private Integer port; private String username; - private String password; private String fromAddress; private Boolean auth; private Boolean startTls; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EmailRecipient.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EmailRecipient.java deleted file mode 100644 index 2850aa97d..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EmailRecipient.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.adityachandel.booklore.model.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Deprecated -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class EmailRecipient { - private Long id; - private String email; - private String name; - private boolean defaultRecipient; -} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/EmailProviderEntity.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/EmailProviderEntity.java deleted file mode 100644 index 4b0e42a6e..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/EmailProviderEntity.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.adityachandel.booklore.model.entity; - -import jakarta.persistence.*; -import lombok.*; - -@Deprecated -@Entity -@Getter -@Setter -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Table(name = "email_provider") -public class EmailProviderEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "name", nullable = false, unique = true) - private String name; - - @Column(name = "host", nullable = false) - private String host; - - @Column(name = "port", nullable = false) - private int port; - - @Column(name = "username", nullable = false) - private String username; - - @Column(name = "password", nullable = false) - private String password; - - @Column(name = "from_address") - private String fromAddress; - - @Column(name = "auth", nullable = false) - private boolean auth; - - @Column(name = "start_tls", nullable = false) - private boolean startTls; - - @Column(name = "is_default", nullable = false) - private boolean defaultProvider; -} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/EmailRecipientEntity.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/EmailRecipientEntity.java deleted file mode 100644 index bb75255ba..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/EmailRecipientEntity.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.adityachandel.booklore.model.entity; - -import jakarta.persistence.*; -import lombok.*; - -@Deprecated -@Entity -@Getter -@Setter -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Table(name = "email_recipient") -public class EmailRecipientEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "email", nullable = false, unique = true) - private String email; - - @Column(name = "name", nullable = false) - private String name; - - @Column(name = "is_default", nullable = false) - private boolean defaultRecipient; -} \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/UserEmailProviderPreferenceEntity.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/UserEmailProviderPreferenceEntity.java new file mode 100644 index 000000000..679cf3c05 --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/UserEmailProviderPreferenceEntity.java @@ -0,0 +1,27 @@ +package com.adityachandel.booklore.model.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "user_email_provider_preference", uniqueConstraints = { + @UniqueConstraint(columnNames = {"user_id"}) +}) +public class UserEmailProviderPreferenceEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "default_provider_id", nullable = false) + private Long defaultProviderId; +} + diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/repository/EmailProviderRepository.java b/booklore-api/src/main/java/com/adityachandel/booklore/repository/EmailProviderRepository.java deleted file mode 100644 index d4dbf06e0..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/repository/EmailProviderRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.adityachandel.booklore.repository; - -import com.adityachandel.booklore.model.entity.EmailProviderEntity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Deprecated -@Repository -public interface EmailProviderRepository extends JpaRepository { - - @Modifying - @Query("UPDATE EmailProviderEntity e SET e.defaultProvider = false") - void updateAllProvidersToNonDefault(); - - @Query("SELECT e FROM EmailProviderEntity e WHERE e.defaultProvider = true") - Optional findDefaultEmailProvider(); -} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/repository/EmailProviderV2Repository.java b/booklore-api/src/main/java/com/adityachandel/booklore/repository/EmailProviderV2Repository.java index 1e14db19d..769a8eb21 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/repository/EmailProviderV2Repository.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/repository/EmailProviderV2Repository.java @@ -2,7 +2,6 @@ package com.adityachandel.booklore.repository; import com.adityachandel.booklore.model.entity.EmailProviderV2Entity; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @@ -17,16 +16,12 @@ public interface EmailProviderV2Repository extends JpaRepository findAllByUserId(Long userId); - @Modifying - @Query("UPDATE EmailProviderV2Entity e SET e.defaultProvider = false") - void updateAllProvidersToNonDefault(); - - @Query("SELECT e FROM EmailProviderV2Entity e WHERE e.userId = :userId AND e.defaultProvider = true") - Optional findDefaultEmailProvider(@Param("userId") Long userId); - @Query("SELECT e FROM EmailProviderV2Entity e WHERE e.shared = true AND e.userId IN (SELECT u.id FROM BookLoreUserEntity u WHERE u.permissions.permissionAdmin = true)") List findAllBySharedTrueAndAdmin(); @Query("SELECT e FROM EmailProviderV2Entity e WHERE e.id = :id AND e.shared = true AND e.userId IN (SELECT u.id FROM BookLoreUserEntity u WHERE u.permissions.permissionAdmin = true)") Optional findSharedProviderById(@Param("id") Long id); + + @Query("SELECT e FROM EmailProviderV2Entity e WHERE e.id = :id AND (e.userId = :userId OR (e.shared = true AND e.userId IN (SELECT u.id FROM BookLoreUserEntity u WHERE u.permissions.permissionAdmin = true)))") + Optional findAccessibleProvider(@Param("id") Long id, @Param("userId") Long userId); } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/repository/EmailRecipientRepository.java b/booklore-api/src/main/java/com/adityachandel/booklore/repository/EmailRecipientRepository.java deleted file mode 100644 index 87512bcc6..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/repository/EmailRecipientRepository.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.adityachandel.booklore.repository; - -import com.adityachandel.booklore.model.entity.EmailRecipientEntity; -import jakarta.transaction.Transactional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Deprecated -@Repository -public interface EmailRecipientRepository extends JpaRepository { - - Optional findById(long id); - - @Modifying - @Transactional - @Query("UPDATE EmailRecipientEntity e SET e.defaultRecipient = false WHERE e.defaultRecipient = true") - void updateAllRecipientsToNonDefault(); - - @Query("SELECT e FROM EmailRecipientEntity e WHERE e.defaultRecipient = true") - Optional findDefaultEmailRecipient(); -} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/repository/EmailRecipientV2Repository.java b/booklore-api/src/main/java/com/adityachandel/booklore/repository/EmailRecipientV2Repository.java index 54bfa62e7..89052e79a 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/repository/EmailRecipientV2Repository.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/repository/EmailRecipientV2Repository.java @@ -1,6 +1,5 @@ package com.adityachandel.booklore.repository; -import com.adityachandel.booklore.model.entity.EmailRecipientEntity; import com.adityachandel.booklore.model.entity.EmailRecipientV2Entity; import jakarta.transaction.Transactional; import org.springframework.data.jpa.repository.JpaRepository; @@ -20,9 +19,9 @@ public interface EmailRecipientV2Repository extends JpaRepository findDefaultEmailRecipient(); + @Query("SELECT e FROM EmailRecipientV2Entity e WHERE e.defaultRecipient = true AND e.userId = :userId") + Optional findDefaultEmailRecipientByUserId(Long userId); } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/repository/UserEmailProviderPreferenceRepository.java b/booklore-api/src/main/java/com/adityachandel/booklore/repository/UserEmailProviderPreferenceRepository.java new file mode 100644 index 000000000..ea143e0ec --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/repository/UserEmailProviderPreferenceRepository.java @@ -0,0 +1,16 @@ +package com.adityachandel.booklore.repository; + +import com.adityachandel.booklore.model.entity.UserEmailProviderPreferenceEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserEmailProviderPreferenceRepository extends JpaRepository { + + Optional findByUserId(Long userId); + + void deleteByUserId(Long userId); +} + diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailProviderService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailProviderService.java deleted file mode 100644 index b5f6573b0..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailProviderService.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.adityachandel.booklore.service.email; - -import com.adityachandel.booklore.exception.ApiError; -import com.adityachandel.booklore.mapper.EmailProviderMapper; -import com.adityachandel.booklore.model.dto.EmailProvider; -import com.adityachandel.booklore.model.dto.request.CreateEmailProviderRequest; -import com.adityachandel.booklore.model.entity.EmailProviderEntity; -import com.adityachandel.booklore.repository.EmailProviderRepository; -import jakarta.transaction.Transactional; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -@Deprecated -@Slf4j -@Service -@AllArgsConstructor -public class EmailProviderService { - - private final EmailProviderRepository emailProviderRepository; - private final EmailProviderMapper emailProviderMapper; - - public EmailProvider getEmailProvider(Long id) { - EmailProviderEntity emailProvider = emailProviderRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id)); - return emailProviderMapper.toDTO(emailProvider); - } - - public EmailProvider createEmailProvider(CreateEmailProviderRequest request) { - boolean isFirstProvider = emailProviderRepository.count() == 0; - EmailProviderEntity emailProviderEntity = emailProviderMapper.toEntity(request); - emailProviderEntity.setDefaultProvider(isFirstProvider); - EmailProviderEntity savedEntity = emailProviderRepository.save(emailProviderEntity); - return emailProviderMapper.toDTO(savedEntity); - } - - public EmailProvider updateEmailProvider(Long id, CreateEmailProviderRequest request) { - EmailProviderEntity existingProvider = emailProviderRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id)); - emailProviderMapper.updateEntityFromRequest(request, existingProvider); - EmailProviderEntity updatedEntity = emailProviderRepository.save(existingProvider); - return emailProviderMapper.toDTO(updatedEntity); - } - - @Transactional - public void setDefaultEmailProvider(Long id) { - EmailProviderEntity emailProvider = emailProviderRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id)); - emailProviderRepository.updateAllProvidersToNonDefault(); - emailProvider.setDefaultProvider(true); - emailProviderRepository.save(emailProvider); - } - - @Transactional - public void deleteEmailProvider(Long id) { - EmailProviderEntity emailProviderToDelete = emailProviderRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id)); - boolean isDefaultProvider = emailProviderToDelete.isDefaultProvider(); - if (isDefaultProvider) { - List allProviders = emailProviderRepository.findAll(); - if (allProviders.size() > 1) { - allProviders.remove(emailProviderToDelete); - EmailProviderEntity newDefaultProvider = allProviders.get(ThreadLocalRandom.current().nextInt(allProviders.size())); - newDefaultProvider.setDefaultProvider(true); - emailProviderRepository.save(newDefaultProvider); - } - } - emailProviderRepository.deleteById(id); - } - - public List getEmailProviders() { - return emailProviderRepository.findAll().stream().map(emailProviderMapper::toDTO).toList(); - } -} \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailProviderV2Service.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailProviderV2Service.java index 80d80619d..12f56364c 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailProviderV2Service.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailProviderV2Service.java @@ -7,7 +7,9 @@ import com.adityachandel.booklore.model.dto.BookLoreUser; import com.adityachandel.booklore.model.dto.EmailProviderV2; import com.adityachandel.booklore.model.dto.request.CreateEmailProviderRequest; import com.adityachandel.booklore.model.entity.EmailProviderV2Entity; +import com.adityachandel.booklore.model.entity.UserEmailProviderPreferenceEntity; import com.adityachandel.booklore.repository.EmailProviderV2Repository; +import com.adityachandel.booklore.repository.UserEmailProviderPreferenceRepository; import jakarta.transaction.Transactional; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -22,71 +24,120 @@ import java.util.concurrent.ThreadLocalRandom; public class EmailProviderV2Service { private final EmailProviderV2Repository repository; + private final UserEmailProviderPreferenceRepository preferenceRepository; private final EmailProviderV2Mapper mapper; private final AuthenticationService authService; public List getEmailProviders() { BookLoreUser user = authService.getAuthenticatedUser(); List userProviders = repository.findAllByUserId(user.getId()); - if (user.getPermissions().isAdmin()) { - return userProviders.stream().map(mapper::toDTO).toList(); + if (!user.getPermissions().isAdmin()) { + List sharedProviders = repository.findAllBySharedTrueAndAdmin(); + userProviders.addAll(sharedProviders); } - List sharedProviders = repository.findAllBySharedTrueAndAdmin(); - userProviders.addAll(sharedProviders); - return userProviders.stream().map(mapper::toDTO).toList(); + + Long defaultProviderId = getDefaultProviderIdForUser(user.getId()); + return userProviders.stream() + .map(entity -> mapper.toDTO(entity, defaultProviderId)) + .toList(); } public EmailProviderV2 getEmailProvider(Long id) { BookLoreUser user = authService.getAuthenticatedUser(); - EmailProviderV2Entity entity = repository.findByIdAndUserId(id, user.getId()).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id)); - return mapper.toDTO(entity); + EmailProviderV2Entity entity = repository.findAccessibleProvider(id, user.getId()) + .orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id)); + + Long defaultProviderId = getDefaultProviderIdForUser(user.getId()); + return mapper.toDTO(entity, defaultProviderId); } + @Transactional public EmailProviderV2 createEmailProvider(CreateEmailProviderRequest request) { BookLoreUser user = authService.getAuthenticatedUser(); - boolean isFirstProvider = repository.count() == 0; EmailProviderV2Entity entity = mapper.toEntity(request); - entity.setDefaultProvider(isFirstProvider); entity.setUserId(user.getId()); entity.setShared(user.getPermissions().isAdmin() && request.isShared()); EmailProviderV2Entity savedEntity = repository.save(entity); - return mapper.toDTO(savedEntity); + + if (preferenceRepository.findByUserId(user.getId()).isEmpty()) { + setDefaultProviderForUser(user.getId(), savedEntity.getId()); + } + + Long defaultProviderId = getDefaultProviderIdForUser(user.getId()); + return mapper.toDTO(savedEntity, defaultProviderId); } + @Transactional public EmailProviderV2 updateEmailProvider(Long id, CreateEmailProviderRequest request) { BookLoreUser user = authService.getAuthenticatedUser(); - EmailProviderV2Entity existingProvider = repository.findByIdAndUserId(id, user.getId()).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id)); + EmailProviderV2Entity existingProvider = repository.findByIdAndUserId(id, user.getId()) + .orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id)); + mapper.updateEntityFromRequest(request, existingProvider); if (user.getPermissions().isAdmin()) { existingProvider.setShared(request.isShared()); } EmailProviderV2Entity updatedEntity = repository.save(existingProvider); - return mapper.toDTO(updatedEntity); + + Long defaultProviderId = getDefaultProviderIdForUser(user.getId()); + return mapper.toDTO(updatedEntity, defaultProviderId); } @Transactional public void setDefaultEmailProvider(Long id) { BookLoreUser user = authService.getAuthenticatedUser(); - EmailProviderV2Entity emailProvider = repository.findByIdAndUserId(id, user.getId()).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id)); - repository.updateAllProvidersToNonDefault(); - emailProvider.setDefaultProvider(true); - repository.save(emailProvider); + // Verify user has access to this provider + repository.findAccessibleProvider(id, user.getId()) + .orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id)); + + setDefaultProviderForUser(user.getId(), id); } @Transactional public void deleteEmailProvider(Long id) { BookLoreUser user = authService.getAuthenticatedUser(); - EmailProviderV2Entity emailProviderToDelete = repository.findByIdAndUserId(id, user.getId()).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id)); - boolean isDefaultProvider = emailProviderToDelete.isDefaultProvider(); - if (isDefaultProvider) { - List allProviders = repository.findAll(); - if (allProviders.size() > 1) { - allProviders.remove(emailProviderToDelete); - EmailProviderV2Entity newDefaultProvider = allProviders.get(ThreadLocalRandom.current().nextInt(allProviders.size())); - newDefaultProvider.setDefaultProvider(true); - repository.save(newDefaultProvider); + repository.findByIdAndUserId(id, user.getId()) + .orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id)); + + List preferencesUsingProvider = + preferenceRepository.findAll().stream() + .filter(pref -> pref.getDefaultProviderId().equals(id)) + .toList(); + + for (UserEmailProviderPreferenceEntity preference : preferencesUsingProvider) { + List availableProviders = getAccessibleProvidersForUser(preference.getUserId()); + availableProviders.removeIf(p -> p.getId().equals(id)); + + if (!availableProviders.isEmpty()) { + EmailProviderV2Entity newDefault = availableProviders.get(ThreadLocalRandom.current().nextInt(availableProviders.size())); + preference.setDefaultProviderId(newDefault.getId()); + preferenceRepository.save(preference); + } else { + preferenceRepository.delete(preference); } } + repository.deleteById(id); } + + private Long getDefaultProviderIdForUser(Long userId) { + return preferenceRepository.findByUserId(userId) + .map(UserEmailProviderPreferenceEntity::getDefaultProviderId) + .orElse(null); + } + + private void setDefaultProviderForUser(Long userId, Long providerId) { + UserEmailProviderPreferenceEntity preference = preferenceRepository.findByUserId(userId) + .orElse(UserEmailProviderPreferenceEntity.builder() + .userId(userId) + .build()); + preference.setDefaultProviderId(providerId); + preferenceRepository.save(preference); + } + + private List getAccessibleProvidersForUser(Long userId) { + List providers = repository.findAllByUserId(userId); + providers.addAll(repository.findAllBySharedTrueAndAdmin()); + return providers; + } } \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailRecipientService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailRecipientService.java deleted file mode 100644 index d44ca3b6b..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailRecipientService.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.adityachandel.booklore.service.email; - -import com.adityachandel.booklore.exception.ApiError; -import com.adityachandel.booklore.mapper.EmailRecipientMapper; -import com.adityachandel.booklore.model.dto.EmailRecipient; -import com.adityachandel.booklore.model.dto.request.CreateEmailRecipientRequest; -import com.adityachandel.booklore.model.entity.EmailRecipientEntity; -import com.adityachandel.booklore.repository.EmailRecipientRepository; -import jakarta.transaction.Transactional; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -@Deprecated -@Slf4j -@Service -@AllArgsConstructor -public class EmailRecipientService { - - private final EmailRecipientRepository emailRecipientRepository; - private final EmailRecipientMapper emailRecipientMapper; - - public EmailRecipient getEmailRecipient(Long id) { - EmailRecipientEntity emailRecipient = emailRecipientRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_RECIPIENT_NOT_FOUND.createException(id)); - return emailRecipientMapper.toDTO(emailRecipient); - } - - @Transactional - public EmailRecipient createEmailRecipient(CreateEmailRecipientRequest request) { - boolean isFirstRecipient = emailRecipientRepository.count() == 0; - if (request.isDefaultRecipient() || isFirstRecipient) { - emailRecipientRepository.updateAllRecipientsToNonDefault(); - } - EmailRecipientEntity emailRecipientEntity = emailRecipientMapper.toEntity(request); - emailRecipientEntity.setDefaultRecipient(request.isDefaultRecipient() || isFirstRecipient); - EmailRecipientEntity savedEntity = emailRecipientRepository.save(emailRecipientEntity); - return emailRecipientMapper.toDTO(savedEntity); - } - - @Transactional - public EmailRecipient updateEmailRecipient(Long id, CreateEmailRecipientRequest request) { - EmailRecipientEntity existingRecipient = emailRecipientRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_RECIPIENT_NOT_FOUND.createException(id)); - if (request.isDefaultRecipient()) { - emailRecipientRepository.updateAllRecipientsToNonDefault(); - } - emailRecipientMapper.updateEntityFromRequest(request, existingRecipient); - EmailRecipientEntity updatedEntity = emailRecipientRepository.save(existingRecipient); - return emailRecipientMapper.toDTO(updatedEntity); - } - - @Transactional - public void setDefaultRecipient(Long id) { - EmailRecipientEntity emailRecipient = emailRecipientRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_RECIPIENT_NOT_FOUND.createException(id)); - emailRecipientRepository.updateAllRecipientsToNonDefault(); - emailRecipient.setDefaultRecipient(true); - emailRecipientRepository.save(emailRecipient); - } - - @Transactional - public void deleteEmailRecipient(Long id) { - EmailRecipientEntity emailRecipientToDelete = emailRecipientRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_RECIPIENT_NOT_FOUND.createException(id)); - boolean isDefaultRecipient = emailRecipientToDelete.isDefaultRecipient(); - if (isDefaultRecipient) { - List allRecipients = emailRecipientRepository.findAll(); - if (allRecipients.size() > 1) { - allRecipients.remove(emailRecipientToDelete); - int randomIndex = ThreadLocalRandom.current().nextInt(allRecipients.size()); - EmailRecipientEntity newDefaultRecipient = allRecipients.get(randomIndex); - newDefaultRecipient.setDefaultRecipient(true); - emailRecipientRepository.save(newDefaultRecipient); - } - } - emailRecipientRepository.deleteById(id); - } - - public List getEmailRecipients() { - return emailRecipientRepository.findAll().stream() - .map(emailRecipientMapper::toDTO) - .toList(); - } -} \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailRecipientV2Service.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailRecipientV2Service.java index 27a83ef4e..c26e97b24 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailRecipientV2Service.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailRecipientV2Service.java @@ -44,7 +44,7 @@ public class EmailRecipientV2Service { BookLoreUser user = authService.getAuthenticatedUser(); boolean isFirstRecipient = repository.count() == 0; if (request.isDefaultRecipient() || isFirstRecipient) { - repository.updateAllRecipientsToNonDefault(); + repository.updateAllRecipientsToNonDefault(user.getId()); } EmailRecipientV2Entity entity = mapper.toEntity(request); entity.setDefaultRecipient(request.isDefaultRecipient() || isFirstRecipient); @@ -58,7 +58,7 @@ public class EmailRecipientV2Service { BookLoreUser user = authService.getAuthenticatedUser(); EmailRecipientV2Entity existingRecipient = repository.findByIdAndUserId(id, user.getId()).orElseThrow(() -> ApiError.EMAIL_RECIPIENT_NOT_FOUND.createException(id)); if (request.isDefaultRecipient()) { - repository.updateAllRecipientsToNonDefault(); + repository.updateAllRecipientsToNonDefault(user.getId()); } mapper.updateEntityFromRequest(request, existingRecipient); EmailRecipientV2Entity updatedEntity = repository.save(existingRecipient); @@ -69,7 +69,7 @@ public class EmailRecipientV2Service { public void setDefaultRecipient(Long id) { BookLoreUser user = authService.getAuthenticatedUser(); EmailRecipientV2Entity emailRecipient = repository.findByIdAndUserId(id, user.getId()).orElseThrow(() -> ApiError.EMAIL_RECIPIENT_NOT_FOUND.createException(id)); - repository.updateAllRecipientsToNonDefault(); + repository.updateAllRecipientsToNonDefault(user.getId()); emailRecipient.setDefaultRecipient(true); repository.save(emailRecipient); } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailService.java deleted file mode 100644 index f82cedde6..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/email/EmailService.java +++ /dev/null @@ -1,177 +0,0 @@ -package com.adityachandel.booklore.service.email; - -import com.adityachandel.booklore.exception.ApiError; -import com.adityachandel.booklore.model.dto.request.SendBookByEmailRequest; -import com.adityachandel.booklore.model.entity.BookEntity; -import com.adityachandel.booklore.model.entity.EmailProviderEntity; -import com.adityachandel.booklore.model.entity.EmailRecipientEntity; -import com.adityachandel.booklore.model.websocket.LogNotification; -import com.adityachandel.booklore.model.websocket.Severity; -import com.adityachandel.booklore.model.websocket.Topic; -import com.adityachandel.booklore.repository.BookRepository; -import com.adityachandel.booklore.repository.EmailProviderRepository; -import com.adityachandel.booklore.repository.EmailRecipientRepository; -import com.adityachandel.booklore.service.NotificationService; -import com.adityachandel.booklore.util.SecurityContextVirtualThread; -import com.adityachandel.booklore.util.FileUtils; -import jakarta.mail.MessagingException; -import jakarta.mail.internet.MimeMessage; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.mail.javamail.JavaMailSenderImpl; -import org.springframework.mail.javamail.MimeMessageHelper; -import org.springframework.stereotype.Service; - -import java.io.File; -import java.util.Properties; - -import static com.adityachandel.booklore.model.websocket.LogNotification.createLogNotification; - -@Deprecated -@Slf4j -@Service -@AllArgsConstructor -public class EmailService { - - private final EmailProviderRepository emailProviderRepository; - private final BookRepository bookRepository; - private final EmailRecipientRepository emailRecipientRepository; - private final NotificationService notificationService; - - public void emailBookQuick(Long bookId) { - BookEntity book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId)); - EmailProviderEntity defaultEmailProvider = emailProviderRepository.findDefaultEmailProvider().orElseThrow(ApiError.DEFAULT_EMAIL_PROVIDER_NOT_FOUND::createException); - EmailRecipientEntity defaultEmailRecipient = emailRecipientRepository.findDefaultEmailRecipient().orElseThrow(ApiError.DEFAULT_EMAIL_RECIPIENT_NOT_FOUND::createException); - sendEmailInVirtualThread(defaultEmailProvider, defaultEmailRecipient.getEmail(), book); - } - - public void emailBook(SendBookByEmailRequest request) { - EmailProviderEntity emailProvider = emailProviderRepository.findById(request.getProviderId()).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(request.getProviderId())); - BookEntity book = bookRepository.findById(request.getBookId()).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(request.getBookId())); - EmailRecipientEntity emailRecipient = emailRecipientRepository.findById(request.getRecipientId()).orElseThrow(() -> ApiError.EMAIL_RECIPIENT_NOT_FOUND.createException(request.getRecipientId())); - sendEmailInVirtualThread(emailProvider, emailRecipient.getEmail(), book); - } - - private void sendEmailInVirtualThread(EmailProviderEntity emailProvider, String recipientEmail, BookEntity book) { - String bookTitle = book.getMetadata().getTitle(); - String logMessage = "Email dispatch initiated for book: " + bookTitle + " to " + recipientEmail; - notificationService.sendMessage(Topic.LOG, LogNotification.info(logMessage)); - log.info(logMessage); - SecurityContextVirtualThread.runWithSecurityContext(() -> { - try { - sendEmail(emailProvider, recipientEmail, book); - String successMessage = "The book: " + bookTitle + " has been successfully sent to " + recipientEmail; - notificationService.sendMessage(Topic.LOG, LogNotification.info(successMessage)); - log.info(successMessage); - } catch (Exception e) { - String errorMessage = "An error occurred while sending the book: " + bookTitle + " to " + recipientEmail + ". Error: " + e.getMessage(); - notificationService.sendMessage(Topic.LOG, LogNotification.error(errorMessage)); - log.error(errorMessage, e); - } - }); - } - - private void sendEmail(EmailProviderEntity emailProvider, String recipientEmail, BookEntity book) throws MessagingException { - JavaMailSenderImpl dynamicMailSender = setupMailSender(emailProvider); - MimeMessage message = dynamicMailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, true); - helper.setFrom(StringUtils.firstNonEmpty(emailProvider.getFromAddress(), emailProvider.getUsername())); - helper.setTo(recipientEmail); - helper.setSubject("Your Book from Booklore: " + book.getMetadata().getTitle()); - helper.setText(generateEmailBody(book.getMetadata().getTitle())); - File bookFile = new File(FileUtils.getBookFullPath(book)); - helper.addAttachment(bookFile.getName(), bookFile); - dynamicMailSender.send(message); - log.info("Book sent successfully to {}", recipientEmail); - } - - private JavaMailSenderImpl setupMailSender(EmailProviderEntity emailProvider) { - JavaMailSenderImpl dynamicMailSender = new JavaMailSenderImpl(); - dynamicMailSender.setHost(emailProvider.getHost()); - dynamicMailSender.setPort(emailProvider.getPort()); - dynamicMailSender.setUsername(emailProvider.getUsername()); - dynamicMailSender.setPassword(emailProvider.getPassword()); - - Properties mailProps = dynamicMailSender.getJavaMailProperties(); - mailProps.put("mail.smtp.auth", emailProvider.isAuth()); - - ConnectionType connectionType = determineConnectionType(emailProvider); - configureConnectionType(mailProps, connectionType, emailProvider); - configureTimeouts(mailProps); - - String debugMode = System.getProperty("mail.debug", "false"); - mailProps.put("mail.debug", debugMode); - - log.info("Email configuration: Host={}, Port={}, Type={}, Timeouts=60s", emailProvider.getHost(), emailProvider.getPort(), connectionType); - - return dynamicMailSender; - } - - private ConnectionType determineConnectionType(EmailProviderEntity emailProvider) { - if (emailProvider.getPort() == 465) { - return ConnectionType.SSL; - } else if (emailProvider.getPort() == 587 && emailProvider.isStartTls()) { - return ConnectionType.STARTTLS; - } else if (emailProvider.isStartTls()) { - return ConnectionType.STARTTLS; - } else { - return ConnectionType.PLAIN; - } - } - - private void configureConnectionType(Properties mailProps, ConnectionType connectionType, EmailProviderEntity emailProvider) { - switch (connectionType) { - case SSL -> { - mailProps.put("mail.transport.protocol", "smtps"); - mailProps.put("mail.smtp.ssl.enable", "true"); - mailProps.put("mail.smtp.ssl.trust", emailProvider.getHost()); - mailProps.put("mail.smtp.starttls.enable", "false"); - mailProps.put("mail.smtp.ssl.protocols", "TLSv1.2,TLSv1.3"); - mailProps.put("mail.smtp.ssl.checkserveridentity", "false"); - mailProps.put("mail.smtp.ssl.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); - mailProps.put("mail.smtp.ssl.socketFactory.fallback", "false"); - } - case STARTTLS -> { - mailProps.put("mail.transport.protocol", "smtp"); - mailProps.put("mail.smtp.starttls.enable", "true"); - mailProps.put("mail.smtp.starttls.required", "true"); - mailProps.put("mail.smtp.ssl.enable", "false"); - } - case PLAIN -> { - mailProps.put("mail.transport.protocol", "smtp"); - mailProps.put("mail.smtp.starttls.enable", "false"); - mailProps.put("mail.smtp.ssl.enable", "false"); - } - } - } - - private void configureTimeouts(Properties mailProps) { - String connectionTimeout = System.getProperty("mail.smtp.connectiontimeout", "60000"); - String socketTimeout = System.getProperty("mail.smtp.timeout", "60000"); - String writeTimeout = System.getProperty("mail.smtp.writetimeout", "60000"); - - mailProps.put("mail.smtp.connectiontimeout", connectionTimeout); - mailProps.put("mail.smtp.timeout", socketTimeout); - mailProps.put("mail.smtp.writetimeout", writeTimeout); - - log.debug("Configured email timeouts: connection={}, socket={}, write={}", - connectionTimeout, socketTimeout, writeTimeout); - } - - private String generateEmailBody(String bookTitle) { - return String.format(""" - Hey there, - - You’ve received a new book from Booklore titled “%s” 📚 - - Grab a comfy spot, maybe a cup of tea ☕, and enjoy the story! - """, bookTitle); - } - - private enum ConnectionType { - SSL, - STARTTLS, - PLAIN - } -} \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/email/SendEmailV2Service.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/email/SendEmailV2Service.java index 009e19be6..17c0438f6 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/email/SendEmailV2Service.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/email/SendEmailV2Service.java @@ -12,6 +12,7 @@ import com.adityachandel.booklore.model.websocket.Topic; import com.adityachandel.booklore.repository.BookRepository; import com.adityachandel.booklore.repository.EmailProviderV2Repository; import com.adityachandel.booklore.repository.EmailRecipientV2Repository; +import com.adityachandel.booklore.repository.UserEmailProviderPreferenceRepository; import com.adityachandel.booklore.service.NotificationService; import com.adityachandel.booklore.util.FileUtils; import com.adityachandel.booklore.util.SecurityContextVirtualThread; @@ -35,6 +36,7 @@ import static com.adityachandel.booklore.model.websocket.LogNotification.createL public class SendEmailV2Service { private final EmailProviderV2Repository emailProviderRepository; + private final UserEmailProviderPreferenceRepository preferenceRepository; private final BookRepository bookRepository; private final EmailRecipientV2Repository emailRecipientRepository; private final NotificationService notificationService; @@ -43,8 +45,8 @@ public class SendEmailV2Service { public void emailBookQuick(Long bookId) { BookLoreUser user = authenticationService.getAuthenticatedUser(); BookEntity book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId)); - EmailProviderV2Entity defaultEmailProvider = emailProviderRepository.findDefaultEmailProvider(user.getId()).orElseThrow(ApiError.DEFAULT_EMAIL_PROVIDER_NOT_FOUND::createException); - EmailRecipientV2Entity defaultEmailRecipient = emailRecipientRepository.findDefaultEmailRecipient().orElseThrow(ApiError.DEFAULT_EMAIL_RECIPIENT_NOT_FOUND::createException); + EmailProviderV2Entity defaultEmailProvider = getDefaultEmailProvider(); + EmailRecipientV2Entity defaultEmailRecipient = emailRecipientRepository.findDefaultEmailRecipientByUserId(user.getId()).orElseThrow(ApiError.DEFAULT_EMAIL_RECIPIENT_NOT_FOUND::createException); sendEmailInVirtualThread(defaultEmailProvider, defaultEmailRecipient.getEmail(), book); } @@ -173,6 +175,17 @@ public class SendEmailV2Service { """, bookTitle); } + private EmailProviderV2Entity getDefaultEmailProvider() { + BookLoreUser user = authenticationService.getAuthenticatedUser(); + + Long defaultProviderId = preferenceRepository.findByUserId(user.getId()) + .map(pref -> pref.getDefaultProviderId()) + .orElseThrow(ApiError.DEFAULT_EMAIL_PROVIDER_NOT_FOUND::createException); + + return emailProviderRepository.findAccessibleProvider(defaultProviderId, user.getId()) + .orElseThrow(ApiError.DEFAULT_EMAIL_PROVIDER_NOT_FOUND::createException); + } + private enum ConnectionType { SSL, STARTTLS, diff --git a/booklore-api/src/main/resources/db/migration/V60__Create_user_email_provider_preference_table.sql b/booklore-api/src/main/resources/db/migration/V60__Create_user_email_provider_preference_table.sql new file mode 100644 index 000000000..5a8059279 --- /dev/null +++ b/booklore-api/src/main/resources/db/migration/V60__Create_user_email_provider_preference_table.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS user_email_provider_preference +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + default_provider_id BIGINT NOT NULL, + CONSTRAINT uq_user_id UNIQUE (user_id), + CONSTRAINT fk_user_email_preference_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + CONSTRAINT fk_user_email_preference_provider FOREIGN KEY (default_provider_id) REFERENCES email_provider_v2 (id) ON DELETE CASCADE +) + diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts index ab614e2a7..4d2b0117a 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts @@ -15,7 +15,7 @@ import {UrlHelperService} from '../../../../../shared/service/url-helper.service import {NgClass} from '@angular/common'; import {UserService} from '../../../../settings/user-management/user.service'; import {filter, Subject} from 'rxjs'; -import {EmailService} from '../../../../settings/email/email.service'; +import {EmailService} from '../../../../settings/email-v2/email.service'; import {TieredMenu} from 'primeng/tieredmenu'; import {BookSenderComponent} from '../../book-sender/book-sender.component'; import {Router} from '@angular/router'; @@ -269,7 +269,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { if (this.hasEmailBookPermission()) { items.push( { - label: 'Send Book', + label: 'Email Book', icon: 'pi pi-envelope', items: [{ label: 'Quick Send', diff --git a/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.html b/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.html index 984e59c8e..0ddada7bc 100644 --- a/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.html +++ b/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.html @@ -21,7 +21,7 @@ diff --git a/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.ts b/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.ts index 53b3c3741..9c5686659 100644 --- a/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.ts +++ b/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.ts @@ -2,9 +2,9 @@ import {Component, inject, OnInit} from '@angular/core'; import {Button} from 'primeng/button'; import {Select} from 'primeng/select'; import {FormsModule} from '@angular/forms'; -import {EmailProvider} from '../../../settings/email/email-provider/email-provider.model'; -import {EmailRecipient} from '../../../settings/email/email-recipient/email-recipient.model'; -import {EmailService} from '../../../settings/email/email.service'; +import {EmailProvider} from '../../../settings/email-v2/email-provider.model'; +import {EmailRecipient} from '../../../settings/email-v2/email-recipient.model'; +import {EmailService} from '../../../settings/email-v2/email.service'; import {DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog'; import {MessageService} from 'primeng/api'; import {EmailV2ProviderService} from '../../../settings/email-v2/email-v2-provider/email-v2-provider.service'; diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts index a497c9535..d50fc33cc 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts @@ -12,7 +12,7 @@ import {SplitButton} from 'primeng/splitbutton'; import {ConfirmationService, MenuItem, MessageService} from 'primeng/api'; import {BookSenderComponent} from '../../../../book/components/book-sender/book-sender.component'; import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog'; -import {EmailService} from '../../../../settings/email/email.service'; +import {EmailService} from '../../../../settings/email-v2/email.service'; import {ShelfAssignerComponent} from '../../../../book/components/shelf-assigner/shelf-assigner.component'; import {Tooltip} from 'primeng/tooltip'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; diff --git a/booklore-ui/src/app/features/settings/email/create-email-provider-dialog/create-email-provider-dialog.component.html b/booklore-ui/src/app/features/settings/email-v2/create-email-provider-dialog/create-email-provider-dialog.component.html similarity index 100% rename from booklore-ui/src/app/features/settings/email/create-email-provider-dialog/create-email-provider-dialog.component.html rename to booklore-ui/src/app/features/settings/email-v2/create-email-provider-dialog/create-email-provider-dialog.component.html diff --git a/booklore-ui/src/app/features/settings/email/create-email-provider-dialog/create-email-provider-dialog.component.scss b/booklore-ui/src/app/features/settings/email-v2/create-email-provider-dialog/create-email-provider-dialog.component.scss similarity index 100% rename from booklore-ui/src/app/features/settings/email/create-email-provider-dialog/create-email-provider-dialog.component.scss rename to booklore-ui/src/app/features/settings/email-v2/create-email-provider-dialog/create-email-provider-dialog.component.scss diff --git a/booklore-ui/src/app/features/settings/email/create-email-provider-dialog/create-email-provider-dialog.component.ts b/booklore-ui/src/app/features/settings/email-v2/create-email-provider-dialog/create-email-provider-dialog.component.ts similarity index 95% rename from booklore-ui/src/app/features/settings/email/create-email-provider-dialog/create-email-provider-dialog.component.ts rename to booklore-ui/src/app/features/settings/email-v2/create-email-provider-dialog/create-email-provider-dialog.component.ts index 5c2c02bd5..0860e665c 100644 --- a/booklore-ui/src/app/features/settings/email/create-email-provider-dialog/create-email-provider-dialog.component.ts +++ b/booklore-ui/src/app/features/settings/email-v2/create-email-provider-dialog/create-email-provider-dialog.component.ts @@ -6,7 +6,7 @@ import {InputText} from 'primeng/inputtext'; import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {MessageService} from 'primeng/api'; import {DynamicDialogRef} from 'primeng/dynamicdialog'; -import {EmailV2ProviderService} from '../../email-v2/email-v2-provider/email-v2-provider.service'; +import {EmailV2ProviderService} from '../email-v2-provider/email-v2-provider.service'; @Component({ selector: 'app-create-email-provider-dialog', diff --git a/booklore-ui/src/app/features/settings/email/create-email-recipient-dialog/create-email-recipient-dialog.component.html b/booklore-ui/src/app/features/settings/email-v2/create-email-recipient-dialog/create-email-recipient-dialog.component.html similarity index 100% rename from booklore-ui/src/app/features/settings/email/create-email-recipient-dialog/create-email-recipient-dialog.component.html rename to booklore-ui/src/app/features/settings/email-v2/create-email-recipient-dialog/create-email-recipient-dialog.component.html diff --git a/booklore-ui/src/app/features/settings/email/create-email-recipient-dialog/create-email-recipient-dialog.component.scss b/booklore-ui/src/app/features/settings/email-v2/create-email-recipient-dialog/create-email-recipient-dialog.component.scss similarity index 100% rename from booklore-ui/src/app/features/settings/email/create-email-recipient-dialog/create-email-recipient-dialog.component.scss rename to booklore-ui/src/app/features/settings/email-v2/create-email-recipient-dialog/create-email-recipient-dialog.component.scss diff --git a/booklore-ui/src/app/features/settings/email/create-email-recipient-dialog/create-email-recipient-dialog.component.ts b/booklore-ui/src/app/features/settings/email-v2/create-email-recipient-dialog/create-email-recipient-dialog.component.ts similarity index 95% rename from booklore-ui/src/app/features/settings/email/create-email-recipient-dialog/create-email-recipient-dialog.component.ts rename to booklore-ui/src/app/features/settings/email-v2/create-email-recipient-dialog/create-email-recipient-dialog.component.ts index 6580c5c6a..dcebdd85f 100644 --- a/booklore-ui/src/app/features/settings/email/create-email-recipient-dialog/create-email-recipient-dialog.component.ts +++ b/booklore-ui/src/app/features/settings/email-v2/create-email-recipient-dialog/create-email-recipient-dialog.component.ts @@ -5,7 +5,7 @@ import {DynamicDialogRef} from 'primeng/dynamicdialog'; import {Checkbox} from 'primeng/checkbox'; import {Button} from 'primeng/button'; import {InputText} from 'primeng/inputtext'; -import {EmailV2RecipientService} from '../../email-v2/email-v2-recipient/email-v2-recipient.service'; +import {EmailV2RecipientService} from '../email-v2-recipient/email-v2-recipient.service'; @Component({ selector: 'app-create-email-recipient-dialog', diff --git a/booklore-ui/src/app/features/settings/email/email-provider/email-provider.model.ts b/booklore-ui/src/app/features/settings/email-v2/email-provider.model.ts similarity index 100% rename from booklore-ui/src/app/features/settings/email/email-provider/email-provider.model.ts rename to booklore-ui/src/app/features/settings/email-v2/email-provider.model.ts diff --git a/booklore-ui/src/app/features/settings/email/email-recipient/email-recipient.model.ts b/booklore-ui/src/app/features/settings/email-v2/email-recipient.model.ts similarity index 100% rename from booklore-ui/src/app/features/settings/email/email-recipient/email-recipient.model.ts rename to booklore-ui/src/app/features/settings/email-v2/email-recipient.model.ts diff --git a/booklore-ui/src/app/features/settings/email-v2/email-v2-provider/email-v2-provider.component.ts b/booklore-ui/src/app/features/settings/email-v2/email-v2-provider/email-v2-provider.component.ts index bb1aca5a1..f1cd305b1 100644 --- a/booklore-ui/src/app/features/settings/email-v2/email-v2-provider/email-v2-provider.component.ts +++ b/booklore-ui/src/app/features/settings/email-v2/email-v2-provider/email-v2-provider.component.ts @@ -9,8 +9,8 @@ import {TableModule} from 'primeng/table'; import {Tooltip} from 'primeng/tooltip'; import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog'; import {EmailV2ProviderService} from './email-v2-provider.service'; -import {CreateEmailProviderDialogComponent} from '../../email/create-email-provider-dialog/create-email-provider-dialog.component'; -import {EmailProvider} from '../../email/email-provider/email-provider.model'; +import {CreateEmailProviderDialogComponent} from '../create-email-provider-dialog/create-email-provider-dialog.component'; +import {EmailProvider} from '../email-provider.model'; import {UserService} from '../../user-management/user.service'; @Component({ @@ -138,13 +138,23 @@ export class EmailV2ProviderComponent implements OnInit { } setDefaultProvider(provider: EmailProvider) { - this.emailProvidersService.setDefaultProvider(provider.id).subscribe(() => { - this.defaultProviderId = provider.id; - this.messageService.add({ - severity: 'success', - summary: 'Default Provider Set', - detail: `${provider.name} is now the default email provider.` - }); + this.emailProvidersService.setDefaultProvider(provider.id).subscribe({ + next: () => { + this.defaultProviderId = provider.id; + this.messageService.add({ + severity: 'success', + summary: 'Default Provider Set', + detail: `${provider.name} is now the default email provider.` + }); + }, + error: (err) => { + console.error('Failed to set default provider', err); + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: `Failed to set ${provider.name} as the default provider. Please try again.` + }); + } }); } diff --git a/booklore-ui/src/app/features/settings/email-v2/email-v2-provider/email-v2-provider.service.ts b/booklore-ui/src/app/features/settings/email-v2/email-v2-provider/email-v2-provider.service.ts index 94d7b5801..1d7007b58 100644 --- a/booklore-ui/src/app/features/settings/email-v2/email-v2-provider/email-v2-provider.service.ts +++ b/booklore-ui/src/app/features/settings/email-v2/email-v2-provider/email-v2-provider.service.ts @@ -2,7 +2,7 @@ import {inject, Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {Observable} from 'rxjs'; import {API_CONFIG} from '../../../../core/config/api-config'; -import {EmailProvider} from '../../email/email-provider/email-provider.model'; +import {EmailProvider} from '../email-provider.model'; @Injectable({ providedIn: 'root' diff --git a/booklore-ui/src/app/features/settings/email-v2/email-v2-recipient/email-v2-recipient.component.ts b/booklore-ui/src/app/features/settings/email-v2/email-v2-recipient/email-v2-recipient.component.ts index 0fc3c9fb2..72310deda 100644 --- a/booklore-ui/src/app/features/settings/email-v2/email-v2-recipient/email-v2-recipient.component.ts +++ b/booklore-ui/src/app/features/settings/email-v2/email-v2-recipient/email-v2-recipient.component.ts @@ -8,8 +8,8 @@ import {TableModule} from 'primeng/table'; import {Tooltip} from 'primeng/tooltip'; import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog'; import {EmailV2RecipientService} from './email-v2-recipient.service'; -import {EmailRecipient} from '../../email/email-recipient/email-recipient.model'; -import {CreateEmailRecipientDialogComponent} from '../../email/create-email-recipient-dialog/create-email-recipient-dialog.component'; +import {EmailRecipient} from '../email-recipient.model'; +import {CreateEmailRecipientDialogComponent} from '../create-email-recipient-dialog/create-email-recipient-dialog.component'; @Component({ selector: 'app-email-v2-recipient', diff --git a/booklore-ui/src/app/features/settings/email-v2/email-v2-recipient/email-v2-recipient.service.ts b/booklore-ui/src/app/features/settings/email-v2/email-v2-recipient/email-v2-recipient.service.ts index 9a1a579d5..ff248e92b 100644 --- a/booklore-ui/src/app/features/settings/email-v2/email-v2-recipient/email-v2-recipient.service.ts +++ b/booklore-ui/src/app/features/settings/email-v2/email-v2-recipient/email-v2-recipient.service.ts @@ -2,7 +2,7 @@ import { inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { API_CONFIG } from '../../../../core/config/api-config'; -import {EmailRecipient} from '../../email/email-recipient/email-recipient.model'; +import {EmailRecipient} from '../email-recipient.model'; @Injectable({ providedIn: 'root' diff --git a/booklore-ui/src/app/features/settings/email-v2/email-v2.component.html b/booklore-ui/src/app/features/settings/email-v2/email-v2.component.html index 39ee128d0..298409fd4 100644 --- a/booklore-ui/src/app/features/settings/email-v2/email-v2.component.html +++ b/booklore-ui/src/app/features/settings/email-v2/email-v2.component.html @@ -6,7 +6,10 @@

- Configure email settings to send books directly to your devices or recipients. Set up email providers (like Gmail, Outlook, or custom SMTP servers) and manage recipient email addresses. In v2, users with email permission can now set up their own email providers and recipients without interfering with other users' configurations. + Configure email settings to send books directly to your devices or recipients. Set up email providers (like Gmail, Outlook, or custom SMTP servers) and manage recipient email addresses. +

+

+ Migration Notice: Support for legacy email has been removed. Please migrate to Email v2 by recreating providers and recipients. In v2, users can create their own private providers and recipients. Optionally, Booklore admins can share providers with users.

diff --git a/booklore-ui/src/app/features/settings/email-v2/email-v2.component.scss b/booklore-ui/src/app/features/settings/email-v2/email-v2.component.scss index 1c3a9e1f1..33876dec1 100644 --- a/booklore-ui/src/app/features/settings/email-v2/email-v2.component.scss +++ b/booklore-ui/src/app/features/settings/email-v2/email-v2.component.scss @@ -30,6 +30,31 @@ margin: 0; } +.migration-notice { + margin-top: 0.5rem; + padding: 0.75rem; + background-color: var(--p-yellow-50); + border-left: 4px solid var(--p-yellow-500); + color: var(--p-yellow-900); + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0 4px 4px 0; + + @media (prefers-color-scheme: dark) { + background-color: rgba(245, 158, 11, 0.15); + color: var(--p-yellow-200); + border-left-color: var(--p-yellow-400); + } + + strong { + color: var(--p-yellow-800); + + @media (prefers-color-scheme: dark) { + color: var(--p-yellow-100); + } + } +} + .access-denied-card { display: flex; align-items: center; diff --git a/booklore-ui/src/app/features/settings/email-v2/email-v2.service.ts b/booklore-ui/src/app/features/settings/email-v2/email-v2.service.ts deleted file mode 100644 index 8ae869854..000000000 --- a/booklore-ui/src/app/features/settings/email-v2/email-v2.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {inject, Injectable} from '@angular/core'; -import {API_CONFIG} from '../../../core/config/api-config'; -import {HttpClient} from '@angular/common/http'; -import {Observable} from 'rxjs'; - -@Injectable({ - providedIn: 'root' -}) -export class EmailV2Service { - - private readonly apiUrl = `${API_CONFIG.BASE_URL}/api/v2/emails`; - - private http = inject(HttpClient); - - emailBook(request: { bookId: number, providerId: number, recipientId: number }): Observable { - return this.http.post(`${this.apiUrl}/send-book`, request); - } - - emailBookQuick(bookId: number): Observable { - return this.http.post(`${this.apiUrl}/send-book/${bookId}`, {}); - } -} diff --git a/booklore-ui/src/app/features/settings/email/email.service.ts b/booklore-ui/src/app/features/settings/email-v2/email.service.ts similarity index 100% rename from booklore-ui/src/app/features/settings/email/email.service.ts rename to booklore-ui/src/app/features/settings/email-v2/email.service.ts diff --git a/booklore-ui/src/app/features/settings/email/email-provider/email-provider.component.html b/booklore-ui/src/app/features/settings/email/email-provider/email-provider.component.html deleted file mode 100644 index 94c70e773..000000000 --- a/booklore-ui/src/app/features/settings/email/email-provider/email-provider.component.html +++ /dev/null @@ -1,258 +0,0 @@ -
-
-
- -
- DEPRECATED - NO LONGER FUNCTIONAL -

- This Email feature has been deprecated and is no longer operational. All functionality has been disabled. - Please migrate to Email v2 immediately to continue sending emails. -

-
-
-
-
-

- - Email Providers -

-

- Configure email-sending services like Gmail, Outlook, or custom SMTP servers for sending books via email. The default email provider will be used for 'Quick Book Send' located in the Book Card menu. -

-
- -
-
-
-
-

- - Current Providers -

- - -
-
- -
- - - - -
- - Default -
- - -
- - Name -
- - -
- - Host -
- - -
- - Port -
- - -
- - Username -
- - -
- - Password -
- - -
- - From Address -
- - Auth - StartTLS - -
- - Edit -
- - -
- - Delete -
- - -
- - - - - - - - - - @if (provider.isEditing) { - - } - @if (!provider.isEditing) { - {{ provider.name }} - } - - - - @if (provider.isEditing) { - - } - @if (!provider.isEditing) { - {{ provider.host }} - } - - - - @if (provider.isEditing) { - - } - @if (!provider.isEditing) { - {{ provider.port }} - } - - - - @if (provider.isEditing) { - - } - @if (!provider.isEditing) { - {{ provider.username }} - } - - - - @if (provider.isEditing) { - - } - @if (!provider.isEditing) { - Hidden - } - - - - @if (provider.isEditing) { - - } - @if (!provider.isEditing) { - {{ provider.fromAddress }} - } - - - - - - - - - - - - - - @if (!provider.isEditing) { - - - } - @if (provider.isEditing) { -
- - - - -
- } - - - - - - - -
- - - - -
- -

No email providers found

-

Create your first email provider to start sending books

-
- - -
-
-
-
-
-
diff --git a/booklore-ui/src/app/features/settings/email/email-provider/email-provider.component.scss b/booklore-ui/src/app/features/settings/email/email-provider/email-provider.component.scss deleted file mode 100644 index 98306e3d1..000000000 --- a/booklore-ui/src/app/features/settings/email/email-provider/email-provider.component.scss +++ /dev/null @@ -1,167 +0,0 @@ -.enclosing-container { - border-color: var(--p-content-border-color); - background: var(--p-content-background); -} - -.settings-header { - margin-top: 1rem; -} - -.settings-title { - display: flex; - align-items: center; - gap: 0.75rem; - font-size: 1.25rem; - font-weight: 700; - color: var(--p-text-color); - margin: 0 0 0.75rem 0; - - .pi { - color: var(--p-primary-color); - font-size: 1.25rem; - } -} - -.settings-description { - color: var(--p-text-muted-color); - font-size: 0.875rem; - line-height: 1.5; - margin-bottom: 1rem; -} - -.settings-content { - display: flex; - flex-direction: column; - gap: 2rem; -} - -.preferences-section { - @media (min-width: 768px) { - padding: 0.5rem 1rem; - } -} - -.section-header { - margin-bottom: 1rem; - - .section-title-group { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 1rem; - - @media (max-width: 767px) { - flex-direction: column; - align-items: flex-start; - gap: 0.75rem; - - p-button { - align-self: flex-end; - } - } - } -} - -.section-title { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 1.125rem; - font-weight: 600; - color: var(--p-text-color); - - .pi { - color: var(--p-primary-color); - } -} - -.table-card { - border: 1px solid var(--p-content-border-color); - border-radius: 8px; - overflow: hidden; - background: var(--p-content-background); -} - -.p-datatable { - .p-datatable-table { - border-collapse: separate; - border-spacing: 0; - } - - .p-datatable-thead > tr > th { - background: var(--p-surface-100); - border-bottom: 2px solid var(--p-content-border-color); - padding: 1rem; - font-weight: 600; - color: var(--p-text-color); - white-space: nowrap; - } - - .p-datatable-tbody > tr { - transition: background-color 0.2s; - - &:hover { - background: var(--p-surface-50); - } - - &:last-child { - border-bottom: none; - } - } - - .p-datatable-tbody > tr > td { - padding: 1rem; - border: none; - } -} - -.p-datatable th .header-content { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 0.5rem; -} - -.permission-header { - text-align: center; - font-size: 0.875rem; - padding: 0.75rem 0.5rem !important; - min-width: 80px; - width: 80px; -} - -.actions-header { - text-align: center; - min-width: 80px; -} - -.actions-cell { - text-align: center; -} - -.password-hidden { - color: var(--p-text-muted-color); - font-style: italic; -} - -.empty-message { - text-align: center; - padding: 2rem 1rem; - color: var(--p-text-muted-color); - - .pi { - font-size: 2rem; - margin-bottom: 1rem; - color: var(--p-surface-400); - } - - .empty-title { - font-size: 1.125rem; - font-weight: 600; - margin-bottom: 0.5rem; - } - - .empty-subtitle { - font-size: 0.875rem; - } -} diff --git a/booklore-ui/src/app/features/settings/email/email-provider/email-provider.component.ts b/booklore-ui/src/app/features/settings/email/email-provider/email-provider.component.ts deleted file mode 100644 index eaa3c585a..000000000 --- a/booklore-ui/src/app/features/settings/email/email-provider/email-provider.component.ts +++ /dev/null @@ -1,139 +0,0 @@ -import {Component, inject, OnInit} from '@angular/core'; -import {Button} from 'primeng/button'; -import {Checkbox} from 'primeng/checkbox'; - -import {MessageService, PrimeTemplate} from 'primeng/api'; -import {RadioButton} from 'primeng/radiobutton'; -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {TableModule} from 'primeng/table'; -import {Tooltip} from 'primeng/tooltip'; -import {EmailProvider} from './email-provider.model'; -import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog'; -import {EmailProviderService} from './email-provider.service'; -import {CreateEmailProviderDialogComponent} from '../create-email-provider-dialog/create-email-provider-dialog.component'; - -@Component({ - selector: 'app-email-provider', - imports: [ - Button, - Checkbox, - PrimeTemplate, - RadioButton, - ReactiveFormsModule, - TableModule, - Tooltip, - FormsModule - ], - templateUrl: './email-provider.component.html', - styleUrl: './email-provider.component.scss' -}) -export class EmailProviderComponent implements OnInit { - emailProviders: EmailProvider[] = []; - editingProviderIds: number[] = []; - ref: DynamicDialogRef | undefined; - private dialogService = inject(DialogService); - private emailProvidersService = inject(EmailProviderService); - private messageService = inject(MessageService); - defaultProviderId: any; - - ngOnInit(): void { - this.loadEmailProviders(); - } - - loadEmailProviders(): void { - this.emailProvidersService.getEmailProviders().subscribe({ - next: (emailProviders: EmailProvider[]) => { - this.emailProviders = emailProviders.map((provider) => ({ - ...provider, - isEditing: false, - })); - const defaultProvider = emailProviders.find((provider) => provider.defaultProvider); - this.defaultProviderId = defaultProvider ? defaultProvider.id : null; - }, - error: () => { - this.messageService.add({ - severity: 'error', - summary: 'Error', - detail: 'Failed to load Email Providers', - }); - }, - }); - } - - toggleEdit(provider: EmailProvider): void { - provider.isEditing = !provider.isEditing; - if (provider.isEditing) { - this.editingProviderIds.push(provider.id); - } else { - this.editingProviderIds = this.editingProviderIds.filter((id) => id !== provider.id); - } - } - - saveProvider(provider: EmailProvider): void { - this.emailProvidersService.updateProvider(provider).subscribe({ - next: () => { - provider.isEditing = false; - this.messageService.add({ - severity: 'success', - summary: 'Success', - detail: 'Provider updated successfully', - }); - this.loadEmailProviders(); - }, - error: () => { - this.messageService.add({ - severity: 'error', - summary: 'Error', - detail: 'Failed to update provider', - }); - }, - }); - } - - deleteProvider(provider: EmailProvider): void { - if (confirm(`Are you sure you want to delete provider "${provider.name}"?`)) { - this.emailProvidersService.deleteProvider(provider.id).subscribe({ - next: () => { - this.messageService.add({ - severity: 'success', - summary: 'Success', - detail: `Provider "${provider.name}" deleted successfully`, - }); - this.loadEmailProviders(); - }, - error: () => { - this.messageService.add({ - severity: 'error', - summary: 'Error', - detail: 'Failed to delete provider', - }); - }, - }); - } - } - - openCreateProviderDialog() { - this.ref = this.dialogService.open(CreateEmailProviderDialogComponent, { - header: 'Create Email Provider', - modal: true, - closable: true, - style: {position: 'absolute', top: '15%'}, - }); - this.ref.onClose.subscribe((result) => { - if (result) { - this.loadEmailProviders(); - } - }); - } - - setDefaultProvider(provider: EmailProvider) { - this.emailProvidersService.setDefaultProvider(provider.id).subscribe(() => { - this.defaultProviderId = provider.id; - this.messageService.add({ - severity: 'success', - summary: 'Default Provider Set', - detail: `${provider.name} is now the default email provider.` - }); - }); - } -} diff --git a/booklore-ui/src/app/features/settings/email/email-provider/email-provider.service.ts b/booklore-ui/src/app/features/settings/email/email-provider/email-provider.service.ts deleted file mode 100644 index 58a618d2a..000000000 --- a/booklore-ui/src/app/features/settings/email/email-provider/email-provider.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {inject, Injectable} from '@angular/core'; -import {HttpClient} from '@angular/common/http'; -import {Observable} from 'rxjs'; -import {EmailProvider} from './email-provider.model'; -import {API_CONFIG} from '../../../../core/config/api-config'; - -@Injectable({ - providedIn: 'root' -}) -export class EmailProviderService { - private readonly url = `${API_CONFIG.BASE_URL}/api/v1/email/providers`; - - private http = inject(HttpClient); - - getEmailProviders(): Observable { - return this.http.get(this.url); - } - - createEmailProvider(provider: EmailProvider): Observable { - return this.http.post(this.url, provider); - } - - updateProvider(provider: EmailProvider): Observable { - return this.http.put(`${this.url}/${provider.id}`, provider); - } - - deleteProvider(id: number): Observable { - return this.http.delete(`${this.url}/${id}`); - } - - setDefaultProvider(id: number): Observable { - return this.http.patch(`${this.url}/${id}/set-default`, {}); - } -} diff --git a/booklore-ui/src/app/features/settings/email/email-recipient/email-recipient.component.html b/booklore-ui/src/app/features/settings/email/email-recipient/email-recipient.component.html deleted file mode 100644 index 564497ebe..000000000 --- a/booklore-ui/src/app/features/settings/email/email-recipient/email-recipient.component.html +++ /dev/null @@ -1,168 +0,0 @@ -
-
-

- - Recipient Emails -

-

- Manage the list of recipients who will receive books via email. The 'Default' recipient will be used for 'Quick Book Send,' located in the Book Card menu. -

-
- -
-
-
-
-

- - Current Recipients -

- - -
-
- -
- - - - -
- - Default -
- - -
- - Email Address -
- - -
- - Name -
- - -
- - Edit -
- - -
- - Delete -
- - -
- - - - - - - - - - @if (recipient.isEditing) { - - } - @if (!recipient.isEditing) { - {{ recipient.email }} - } - - - - @if (recipient.isEditing) { - - } - @if (!recipient.isEditing) { - {{ recipient.name }} - } - - - - @if (!recipient.isEditing) { - - - } - @if (recipient.isEditing) { -
- - - - -
- } - - - - - - - -
- - - - -
- -

No recipients found

-

Add your first email recipient to start sending books

-
- - -
-
-
-
-
-
diff --git a/booklore-ui/src/app/features/settings/email/email-recipient/email-recipient.component.scss b/booklore-ui/src/app/features/settings/email/email-recipient/email-recipient.component.scss deleted file mode 100644 index b07197096..000000000 --- a/booklore-ui/src/app/features/settings/email/email-recipient/email-recipient.component.scss +++ /dev/null @@ -1,144 +0,0 @@ -.enclosing-container { - border-color: var(--p-content-border-color); - background: var(--p-content-background); -} - -.settings-header { - margin-top: 1rem; -} - -.settings-title { - display: flex; - align-items: center; - gap: 0.75rem; - font-size: 1.25rem; - font-weight: 700; - color: var(--p-text-color); - margin: 0 0 0.75rem 0; - - .pi { - color: var(--p-primary-color); - font-size: 1.25rem; - } -} - -.settings-description { - color: var(--p-text-muted-color); - font-size: 0.875rem; - line-height: 1.5; - margin-bottom: 1rem; -} - -.settings-content { - display: flex; - flex-direction: column; - gap: 2rem; -} - -.preferences-section { - @media (min-width: 768px) { - padding: 0 1rem; - } -} - -.section-header { - margin-bottom: 1rem; - - .section-title-group { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 1rem; - } -} - -.section-title { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 1.125rem; - font-weight: 600; - color: var(--p-text-color); - - .pi { - color: var(--p-primary-color); - } -} - -.table-card { - border: 1px solid var(--p-content-border-color); - border-radius: 8px; - overflow: hidden; - background: var(--p-content-background); -} - -.p-datatable { - .p-datatable-table { - border-collapse: separate; - border-spacing: 0; - } - - .p-datatable-thead > tr > th { - background: var(--p-surface-100); - border-bottom: 2px solid var(--p-content-border-color); - padding: 1rem; - font-weight: 600; - color: var(--p-text-color); - white-space: nowrap; - } - - .p-datatable-tbody > tr { - transition: background-color 0.2s; - - &:hover { - background: var(--p-surface-50); - } - - &:last-child { - border-bottom: none; - } - } - - .p-datatable-tbody > tr > td { - padding: 1rem; - border: none; - } -} - -.p-datatable th .header-content { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 0.5rem; -} - -.actions-header { - text-align: center; - min-width: 80px; -} - -.actions-cell { - text-align: center; -} - -.empty-message { - text-align: center; - padding: 2rem 1rem; - color: var(--p-text-muted-color); - - .pi { - font-size: 2rem; - margin-bottom: 1rem; - color: var(--p-surface-400); - } - - .empty-title { - font-size: 1.125rem; - font-weight: 600; - margin-bottom: 0.5rem; - } - - .empty-subtitle { - font-size: 0.875rem; - } -} diff --git a/booklore-ui/src/app/features/settings/email/email-recipient/email-recipient.component.ts b/booklore-ui/src/app/features/settings/email/email-recipient/email-recipient.component.ts deleted file mode 100644 index 6e27e47be..000000000 --- a/booklore-ui/src/app/features/settings/email/email-recipient/email-recipient.component.ts +++ /dev/null @@ -1,140 +0,0 @@ -import {Component, inject, OnInit} from '@angular/core'; -import {Button} from 'primeng/button'; - -import {MessageService, PrimeTemplate} from 'primeng/api'; -import {RadioButton} from 'primeng/radiobutton'; -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {TableModule} from 'primeng/table'; -import {Tooltip} from 'primeng/tooltip'; -import {EmailProvider} from '../email-provider/email-provider.model'; -import {EmailRecipient} from './email-recipient.model'; -import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog'; -import {EmailProviderService} from '../email-provider/email-provider.service'; -import {EmailRecipientService} from './email-recipient.service'; -import {CreateEmailProviderDialogComponent} from '../create-email-provider-dialog/create-email-provider-dialog.component'; -import {CreateEmailRecipientDialogComponent} from '../create-email-recipient-dialog/create-email-recipient-dialog.component'; - -@Component({ - selector: 'app-email-recipient', - imports: [ - Button, - PrimeTemplate, - RadioButton, - ReactiveFormsModule, - TableModule, - Tooltip, - FormsModule -], - templateUrl: './email-recipient.component.html', - styleUrl: './email-recipient.component.scss' -}) -export class EmailRecipientComponent implements OnInit { - recipientEmails: EmailRecipient[] = []; - editingRecipientIds: number[] = []; - ref: DynamicDialogRef | undefined; - private dialogService = inject(DialogService); - private emailRecipientService = inject(EmailRecipientService); - private messageService = inject(MessageService); - defaultRecipientId: any; - - ngOnInit(): void { - this.loadRecipientEmails(); - } - - loadRecipientEmails(): void { - this.emailRecipientService.getRecipients().subscribe({ - next: (recipients: EmailRecipient[]) => { - this.recipientEmails = recipients.map((recipient) => ({ - ...recipient, - isEditing: false, - })); - const defaultRecipient = recipients.find((recipient) => recipient.defaultRecipient); - this.defaultRecipientId = defaultRecipient ? defaultRecipient.id : null; - }, - error: () => { - this.messageService.add({ - severity: 'error', - summary: 'Error', - detail: 'Failed to load recipient emails', - }); - }, - }); - } - - toggleEditRecipient(recipient: EmailRecipient): void { - recipient.isEditing = !recipient.isEditing; - if (recipient.isEditing) { - this.editingRecipientIds.push(recipient.id); - } else { - this.editingRecipientIds = this.editingRecipientIds.filter((id) => id !== recipient.id); - } - } - - saveRecipient(recipient: EmailRecipient): void { - this.emailRecipientService.updateRecipient(recipient).subscribe({ - next: () => { - recipient.isEditing = false; - this.messageService.add({ - severity: 'success', - summary: 'Success', - detail: 'Recipient updated successfully', - }); - this.loadRecipientEmails(); - }, - error: () => { - this.messageService.add({ - severity: 'error', - summary: 'Error', - detail: 'Failed to update recipient', - }); - }, - }); - } - - deleteRecipient(recipient: EmailRecipient): void { - if (confirm(`Are you sure you want to delete recipient "${recipient.email}"?`)) { - this.emailRecipientService.deleteRecipient(recipient.id).subscribe({ - next: () => { - this.messageService.add({ - severity: 'success', - summary: 'Success', - detail: `Recipient "${recipient.email}" deleted successfully`, - }); - this.loadRecipientEmails(); - }, - error: () => { - this.messageService.add({ - severity: 'error', - summary: 'Error', - detail: 'Failed to delete recipient', - }); - }, - }); - } - } - - openAddRecipientDialog() { - this.ref = this.dialogService.open(CreateEmailRecipientDialogComponent, { - header: 'Add New Recipient', - modal: true, - closable: true, - style: {position: 'absolute', top: '15%'}, - }); - this.ref.onClose.subscribe((result) => { - if (result) { - this.loadRecipientEmails(); - } - }); - } - - setDefaultRecipient(recipient: EmailRecipient) { - this.emailRecipientService.setDefaultRecipient(recipient.id).subscribe(() => { - this.defaultRecipientId = recipient.id; - this.messageService.add({ - severity: 'success', - summary: 'Default Recipient Set', - detail: `${recipient.email} is now the default recipient.` - }); - }); - } -} diff --git a/booklore-ui/src/app/features/settings/email/email-recipient/email-recipient.service.ts b/booklore-ui/src/app/features/settings/email/email-recipient/email-recipient.service.ts deleted file mode 100644 index 56f804a21..000000000 --- a/booklore-ui/src/app/features/settings/email/email-recipient/email-recipient.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { inject, Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { API_CONFIG } from '../../../../core/config/api-config'; -import {EmailRecipient} from './email-recipient.model'; - -@Injectable({ - providedIn: 'root' -}) -export class EmailRecipientService { - private readonly url = `${API_CONFIG.BASE_URL}/api/v1/email/recipients`; - - private http = inject(HttpClient); - - getRecipients(): Observable { - return this.http.get(this.url); - } - - createRecipient(recipient: EmailRecipient): Observable { - return this.http.post(this.url, recipient); - } - - updateRecipient(recipient: EmailRecipient): Observable { - return this.http.put(`${this.url}/${recipient.id}`, recipient); - } - - deleteRecipient(id: number): Observable { - return this.http.delete(`${this.url}/${id}`); - } - - setDefaultRecipient(id: number): Observable { - return this.http.patch(`${this.url}/${id}/set-default`, {}); - } -} diff --git a/booklore-ui/src/app/features/settings/email/email.component.html b/booklore-ui/src/app/features/settings/email/email.component.html deleted file mode 100644 index be17bb619..000000000 --- a/booklore-ui/src/app/features/settings/email/email.component.html +++ /dev/null @@ -1,9 +0,0 @@ -
-
- -
- -
- -
-
diff --git a/booklore-ui/src/app/features/settings/email/email.component.scss b/booklore-ui/src/app/features/settings/email/email.component.scss deleted file mode 100644 index b886b1f68..000000000 --- a/booklore-ui/src/app/features/settings/email/email.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.enclosing-container { - border-color: var(--p-content-border-color); -} diff --git a/booklore-ui/src/app/features/settings/email/email.component.ts b/booklore-ui/src/app/features/settings/email/email.component.ts deleted file mode 100644 index bdac89437..000000000 --- a/booklore-ui/src/app/features/settings/email/email.component.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {Component} from '@angular/core'; -import {FormsModule} from '@angular/forms'; -import {TableModule} from 'primeng/table'; -import {EmailProviderComponent} from './email-provider/email-provider.component'; -import {EmailRecipientComponent} from './email-recipient/email-recipient.component'; -import {Divider} from 'primeng/divider'; - -@Component({ - selector: 'app-email', - imports: [ - FormsModule, - TableModule, - EmailProviderComponent, - EmailRecipientComponent, - Divider - ], - templateUrl: './email.component.html', - styleUrls: ['./email.component.scss'], -}) -export class EmailComponent { - -} diff --git a/booklore-ui/src/app/features/settings/settings.component.html b/booklore-ui/src/app/features/settings/settings.component.html index fdcbfa45c..1d33ec1c5 100644 --- a/booklore-ui/src/app/features/settings/settings.component.html +++ b/booklore-ui/src/app/features/settings/settings.component.html @@ -20,10 +20,7 @@ Application - User - - - Email + Users } @@ -68,9 +65,6 @@ - - - } diff --git a/booklore-ui/src/app/features/settings/settings.component.ts b/booklore-ui/src/app/features/settings/settings.component.ts index e54816519..a49e2afc6 100644 --- a/booklore-ui/src/app/features/settings/settings.component.ts +++ b/booklore-ui/src/app/features/settings/settings.component.ts @@ -2,7 +2,6 @@ import {Component, inject, OnDestroy, OnInit} from '@angular/core'; import {Tab, TabList, TabPanel, TabPanels, Tabs} from 'primeng/tabs'; import {UserService} from './user-management/user.service'; import {AsyncPipe} from '@angular/common'; -import {EmailComponent} from './email/email.component'; import {GlobalPreferencesComponent} from './global-preferences/global-preferences.component'; import {ActivatedRoute, Router} from '@angular/router'; import {Subscription} from 'rxjs'; @@ -23,7 +22,6 @@ export enum SettingsTab { ViewPreferences = 'view', DeviceSettings = 'device', UserManagement = 'user', - EmailSettings = 'email', EmailSettingsV2 = 'email-v2', NamingPattern = 'naming-pattern', MetadataSettings = 'metadata', @@ -31,7 +29,7 @@ export enum SettingsTab { ApplicationSettings = 'application', AuthenticationSettings = 'authentication', OpdsV2 = 'opds', - Tasks = 'tasks', + Tasks = 'task', } @Component({ @@ -43,7 +41,6 @@ export enum SettingsTab { TabPanels, TabPanel, AsyncPipe, - EmailComponent, GlobalPreferencesComponent, UserManagementComponent, AuthenticationSettingsComponent,