| author | Thorsten Ortlepp
<post@ortlepp.ms> 2025-12-09 21:31:00 UTC |
| committer | Thorsten Ortlepp
<post@ortlepp.ms> 2025-12-09 21:31:00 UTC |
| parent | 89e2902a525ccd60410eae5cc3d1f488c3b35d11 |
| src/main/java/dev/rubidium/subscriptiontool/SubscriptionToolApplication.java | +4 | -1 |
| src/main/java/dev/rubidium/subscriptiontool/controller/SubscriptionController.java | +29 | -9 |
| src/main/java/dev/rubidium/subscriptiontool/model/UnsentMail.java | +5 | -0 |
| src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Mail.java | +71 | -0 |
| src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Subscription.java | +82 | -0 |
| src/main/java/dev/rubidium/subscriptiontool/persistence/repository/MailRepository.java | +15 | -0 |
| src/main/java/dev/rubidium/subscriptiontool/persistence/repository/SubscriptionRepository.java | +14 | -0 |
| src/main/java/dev/rubidium/subscriptiontool/properties/MailProperties.java | +8 | -0 |
| src/main/java/dev/rubidium/subscriptiontool/properties/Translation.java | +3 | -1 |
| src/main/java/dev/rubidium/subscriptiontool/scheduler/MailScheduler.java | +45 | -0 |
| src/main/java/dev/rubidium/subscriptiontool/service/CodeService.java | +6 | -0 |
| src/main/java/dev/rubidium/subscriptiontool/service/MailService.java | +6 | -0 |
| src/main/java/dev/rubidium/subscriptiontool/service/PersistenceService.java | +17 | -0 |
| src/main/java/dev/rubidium/subscriptiontool/service/SubscriptionService.java | +10 | -0 |
| src/main/java/dev/rubidium/subscriptiontool/service/impl/CodeServiceImpl.java | +15 | -0 |
| src/main/java/dev/rubidium/subscriptiontool/service/impl/MailServiceImpl.java | +37 | -0 |
| src/main/java/dev/rubidium/subscriptiontool/service/impl/PersistenceServiceImpl.java | +102 | -0 |
| src/main/java/dev/rubidium/subscriptiontool/service/impl/SubscriptionServiceImpl.java | +43 | -0 |
| src/main/resources/application.properties | +17 | -1 |
| src/main/resources/db/migration/V1_0__create_tables.sql | +1 | -1 |
| src/main/resources/translation.properties | +2 | -0 |
| src/test/java/dev/rubidium/subscriptiontool/service/CodeServiceTest.java | +27 | -0 |
diff --git a/src/main/java/dev/rubidium/subscriptiontool/SubscriptionToolApplication.java b/src/main/java/dev/rubidium/subscriptiontool/SubscriptionToolApplication.java index f15a1da..a61eab7 100644 --- a/src/main/java/dev/rubidium/subscriptiontool/SubscriptionToolApplication.java +++ b/src/main/java/dev/rubidium/subscriptiontool/SubscriptionToolApplication.java @@ -1,12 +1,15 @@ package dev.rubidium.subscriptiontool; +import dev.rubidium.subscriptiontool.properties.MailProperties; import dev.rubidium.subscriptiontool.properties.Translation; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication -@EnableConfigurationProperties(Translation.class) +@EnableConfigurationProperties({Translation.class, MailProperties.class}) +@EnableScheduling public class SubscriptionToolApplication { public static void main(String[] args) { diff --git a/src/main/java/dev/rubidium/subscriptiontool/controller/SubscriptionController.java b/src/main/java/dev/rubidium/subscriptiontool/controller/SubscriptionController.java index d7a77f0..31697e2 100644 --- a/src/main/java/dev/rubidium/subscriptiontool/controller/SubscriptionController.java +++ b/src/main/java/dev/rubidium/subscriptiontool/controller/SubscriptionController.java @@ -2,6 +2,7 @@ package dev.rubidium.subscriptiontool.controller; import dev.rubidium.subscriptiontool.model.Subscription; import dev.rubidium.subscriptiontool.properties.Translation; +import dev.rubidium.subscriptiontool.service.SubscriptionService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -17,9 +18,12 @@ public class SubscriptionController { private static final String ATTRIBUTE_NAME_SUCCESS = "success"; private static final String ATTRIBUTE_NAME_EMAIL = "email"; + private final SubscriptionService subscriptionService; private final Translation translation; - public SubscriptionController(final Translation translation) { + public SubscriptionController(final SubscriptionService subscriptionService, + final Translation translation) { + this.subscriptionService = subscriptionService; this.translation = translation; } @@ -41,10 +45,14 @@ public class SubscriptionController { @PostMapping("/subscribe") public String subscribeSave(@ModelAttribute Subscription subscription, Model model) { - // TODO implement saving subscription + boolean saved = false; + String email = cleanInput(subscription.getEmail()); + if (!email.isEmpty()) { + saved = subscriptionService.saveSubscription(email); + } model.addAttribute(ATTRIBUTE_NAME_TRANSLATION, translation); - model.addAttribute(ATTRIBUTE_NAME_EMAIL, subscription.getEmail()); - model.addAttribute(ATTRIBUTE_NAME_SUCCESS, true); + model.addAttribute(ATTRIBUTE_NAME_EMAIL, email); + model.addAttribute(ATTRIBUTE_NAME_SUCCESS, saved); return "SubscribeSave"; } @@ -59,19 +67,31 @@ public class SubscriptionController { @PostMapping("/unsubscribe") public String unsubscribeDelete(@ModelAttribute Subscription subscription, Model model) { - // TODO implement deleting subscription + boolean deleted = false; + String email = cleanInput(subscription.getEmail()); + if (!email.isEmpty()) { + deleted = subscriptionService.deleteSubscription(email); + } model.addAttribute(ATTRIBUTE_NAME_TRANSLATION, translation); - model.addAttribute(ATTRIBUTE_NAME_EMAIL, subscription.getEmail()); - model.addAttribute(ATTRIBUTE_NAME_SUCCESS, true); + model.addAttribute(ATTRIBUTE_NAME_EMAIL, email); + model.addAttribute(ATTRIBUTE_NAME_SUCCESS, deleted); return "UnsubscribeDelete"; } @GetMapping("/confirm") public String confirm(@RequestParam(name = "code") String code, Model model) { - // TODO implement confirming subscription + boolean confirmed = false; + String confirmationCode = cleanInput(code); + if (!confirmationCode.isEmpty()) { + confirmed = subscriptionService.confirmSubscription(confirmationCode); + } model.addAttribute(ATTRIBUTE_NAME_TRANSLATION, translation); - model.addAttribute(ATTRIBUTE_NAME_SUCCESS, true); + model.addAttribute(ATTRIBUTE_NAME_SUCCESS, confirmed); return "Confirm"; } + + private String cleanInput(String input) { + return input == null ? "" : input.trim(); + } } diff --git a/src/main/java/dev/rubidium/subscriptiontool/model/UnsentMail.java b/src/main/java/dev/rubidium/subscriptiontool/model/UnsentMail.java new file mode 100644 index 0000000..c095cc0 --- /dev/null +++ b/src/main/java/dev/rubidium/subscriptiontool/model/UnsentMail.java @@ -0,0 +1,5 @@ +package dev.rubidium.subscriptiontool.model; + +public record UnsentMail(Long id, String email, String code) { + +} diff --git a/src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Mail.java b/src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Mail.java new file mode 100644 index 0000000..4f90e39 --- /dev/null +++ b/src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Mail.java @@ -0,0 +1,71 @@ +package dev.rubidium.subscriptiontool.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.sql.Timestamp; + +@Entity +@Table(name = "mailqueue") +public class Mail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "subscription") + private Long subscription; + + @Column(name = "sent") + private Boolean sent; + + @Column(name = "creation") + private Timestamp creation; + + @Column(name = "completion") + private Timestamp completion; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getSubscription() { + return subscription; + } + + public void setSubscription(Long subscription) { + this.subscription = subscription; + } + + public Boolean getSent() { + return sent; + } + + public void setSent(Boolean sent) { + this.sent = sent; + } + + public Timestamp getCreation() { + return creation; + } + + public void setCreation(Timestamp creation) { + this.creation = creation; + } + + public Timestamp getCompletion() { + return completion; + } + + public void setCompletion(Timestamp completion) { + this.completion = completion; + } +} diff --git a/src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Subscription.java b/src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Subscription.java new file mode 100644 index 0000000..e300d56 --- /dev/null +++ b/src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Subscription.java @@ -0,0 +1,82 @@ +package dev.rubidium.subscriptiontool.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.sql.Timestamp; + +@Entity +@Table(name = "subscriptions") +public class Subscription { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "mail") + private String mail; + + @Column(name = "code") + private String code; + + @Column(name = "confirmed") + private Boolean confirmed; + + @Column(name = "registration") + private Timestamp registration; + + @Column(name = "confirmation") + private Timestamp confirmation; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getMail() { + return mail; + } + + public void setMail(String mail) { + this.mail = mail; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public Boolean getConfirmed() { + return confirmed; + } + + public void setConfirmed(Boolean confirmed) { + this.confirmed = confirmed; + } + + public Timestamp getRegistration() { + return registration; + } + + public void setRegistration(Timestamp registration) { + this.registration = registration; + } + + public Timestamp getConfirmation() { + return confirmation; + } + + public void setConfirmation(Timestamp confirmation) { + this.confirmation = confirmation; + } +} diff --git a/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/MailRepository.java b/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/MailRepository.java new file mode 100644 index 0000000..3e77673 --- /dev/null +++ b/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/MailRepository.java @@ -0,0 +1,15 @@ +package dev.rubidium.subscriptiontool.persistence.repository; + +import dev.rubidium.subscriptiontool.persistence.entity.Mail; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface MailRepository extends JpaRepository<Mail, Long> { + + @Query("SELECT m FROM Mail m WHERE m.subscription = :id") + Mail findBySubscription(Long id); + + @Query("SELECT m FROM Mail m WHERE m.sent = false") + List<Mail> findUnsentMails(); +} diff --git a/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/SubscriptionRepository.java b/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/SubscriptionRepository.java new file mode 100644 index 0000000..51768c9 --- /dev/null +++ b/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/SubscriptionRepository.java @@ -0,0 +1,14 @@ +package dev.rubidium.subscriptiontool.persistence.repository; + +import dev.rubidium.subscriptiontool.persistence.entity.Subscription; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface SubscriptionRepository extends JpaRepository<Subscription, Long> { + + @Query("SELECT s FROM Subscription s WHERE s.mail = :mail") + Subscription findByMail(String mail); + + @Query("SELECT s FROM Subscription s WHERE s.code = :code") + Subscription findByCode(String code); +} diff --git a/src/main/java/dev/rubidium/subscriptiontool/properties/MailProperties.java b/src/main/java/dev/rubidium/subscriptiontool/properties/MailProperties.java new file mode 100644 index 0000000..0ec2e9c --- /dev/null +++ b/src/main/java/dev/rubidium/subscriptiontool/properties/MailProperties.java @@ -0,0 +1,8 @@ +package dev.rubidium.subscriptiontool.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "subscriptiontool.mail") +public record MailProperties(String from, String url) { + +} diff --git a/src/main/java/dev/rubidium/subscriptiontool/properties/Translation.java b/src/main/java/dev/rubidium/subscriptiontool/properties/Translation.java index 89d5ab0..5e70396 100644 --- a/src/main/java/dev/rubidium/subscriptiontool/properties/Translation.java +++ b/src/main/java/dev/rubidium/subscriptiontool/properties/Translation.java @@ -16,6 +16,8 @@ public record Translation(String title, String confirmSuccess, String confirmError, String emailPlaceholder, - String back) { + String back, + String mailSubject, + String mailText) { } diff --git a/src/main/java/dev/rubidium/subscriptiontool/scheduler/MailScheduler.java b/src/main/java/dev/rubidium/subscriptiontool/scheduler/MailScheduler.java new file mode 100644 index 0000000..fa2c4cc --- /dev/null +++ b/src/main/java/dev/rubidium/subscriptiontool/scheduler/MailScheduler.java @@ -0,0 +1,45 @@ +package dev.rubidium.subscriptiontool.scheduler; + +import dev.rubidium.subscriptiontool.properties.MailProperties; +import dev.rubidium.subscriptiontool.properties.Translation; +import dev.rubidium.subscriptiontool.service.MailService; +import dev.rubidium.subscriptiontool.service.PersistenceService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Service +public class MailScheduler { + + private static final Logger logger = LoggerFactory.getLogger(MailScheduler.class); + + private final PersistenceService persistenceService; + private final MailService mailService; + private final Translation translation; + private final MailProperties mailProperties; + + public MailScheduler(final PersistenceService persistenceService, + final MailService mailService, final Translation translation, + final MailProperties mailProperties) { + this.persistenceService = persistenceService; + this.mailService = mailService; + this.translation = translation; + this.mailProperties = mailProperties; + } + + @Scheduled(cron = "${subscriptiontool.scheduler.cron}", + zone = "${subscriptiontool.scheduler.zone}") + public void sendMails() { + persistenceService.getUnsentMails().forEach(mail -> { + logger.info("Sending double opt-in mail for subscriber # {}", mail.id()); + String link = mailProperties.url() + mail.code(); + String message = translation.mailText().formatted(link); + if (mailService.sendMail( + mailProperties.from(), mail.email(), translation.mailSubject(), message)) { + persistenceService.updateMail(mail.id()); + } + }); + } + +} diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/CodeService.java b/src/main/java/dev/rubidium/subscriptiontool/service/CodeService.java new file mode 100644 index 0000000..3224e3a --- /dev/null +++ b/src/main/java/dev/rubidium/subscriptiontool/service/CodeService.java @@ -0,0 +1,6 @@ +package dev.rubidium.subscriptiontool.service; + +public interface CodeService { + + String generateCode(String input); +} diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/MailService.java b/src/main/java/dev/rubidium/subscriptiontool/service/MailService.java new file mode 100644 index 0000000..1d31e4c --- /dev/null +++ b/src/main/java/dev/rubidium/subscriptiontool/service/MailService.java @@ -0,0 +1,6 @@ +package dev.rubidium.subscriptiontool.service; + +public interface MailService { + + boolean sendMail(String from, String to, String subject, String body); +} diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/PersistenceService.java b/src/main/java/dev/rubidium/subscriptiontool/service/PersistenceService.java new file mode 100644 index 0000000..1e6890b --- /dev/null +++ b/src/main/java/dev/rubidium/subscriptiontool/service/PersistenceService.java @@ -0,0 +1,17 @@ +package dev.rubidium.subscriptiontool.service; + +import dev.rubidium.subscriptiontool.model.UnsentMail; +import java.util.List; + +public interface PersistenceService { + + boolean createSubscription(String email, String code); + + boolean deleteSubscription(String email); + + boolean updateSubscription(String code); + + List<UnsentMail> getUnsentMails(); + + void updateMail(Long id); +} diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/SubscriptionService.java b/src/main/java/dev/rubidium/subscriptiontool/service/SubscriptionService.java new file mode 100644 index 0000000..9662616 --- /dev/null +++ b/src/main/java/dev/rubidium/subscriptiontool/service/SubscriptionService.java @@ -0,0 +1,10 @@ +package dev.rubidium.subscriptiontool.service; + +public interface SubscriptionService { + + boolean saveSubscription(String email); + + boolean deleteSubscription(String email); + + boolean confirmSubscription(String code); +} diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/impl/CodeServiceImpl.java b/src/main/java/dev/rubidium/subscriptiontool/service/impl/CodeServiceImpl.java new file mode 100644 index 0000000..0743e20 --- /dev/null +++ b/src/main/java/dev/rubidium/subscriptiontool/service/impl/CodeServiceImpl.java @@ -0,0 +1,15 @@ +package dev.rubidium.subscriptiontool.service.impl; + +import dev.rubidium.subscriptiontool.service.CodeService; +import java.util.UUID; +import org.springframework.stereotype.Service; + +@Service +public class CodeServiceImpl implements CodeService { + + @Override + public String generateCode(String input) { + UUID uuid = UUID.nameUUIDFromBytes(input.getBytes()); + return uuid.toString().replace("-", ""); + } +} diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/impl/MailServiceImpl.java b/src/main/java/dev/rubidium/subscriptiontool/service/impl/MailServiceImpl.java new file mode 100644 index 0000000..df031cd --- /dev/null +++ b/src/main/java/dev/rubidium/subscriptiontool/service/impl/MailServiceImpl.java @@ -0,0 +1,37 @@ +package dev.rubidium.subscriptiontool.service.impl; + +import dev.rubidium.subscriptiontool.service.MailService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.mail.MailException; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Service +public class MailServiceImpl implements MailService { + + private static final Logger logger = LoggerFactory.getLogger(MailServiceImpl.class); + + private final JavaMailSender mailSender; + + public MailServiceImpl(final JavaMailSender mailSender) { + this.mailSender = mailSender; + } + + @Override + public boolean sendMail(String from, String to, String subject, String body) { + try { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(from); + message.setTo(to); + message.setSubject(subject); + message.setText(body); + mailSender.send(message); + return true; + } catch (MailException ex) { + logger.error("Error sending mail", ex); + return false; + } + } +} diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/impl/PersistenceServiceImpl.java b/src/main/java/dev/rubidium/subscriptiontool/service/impl/PersistenceServiceImpl.java new file mode 100644 index 0000000..845c82c --- /dev/null +++ b/src/main/java/dev/rubidium/subscriptiontool/service/impl/PersistenceServiceImpl.java @@ -0,0 +1,102 @@ +package dev.rubidium.subscriptiontool.service.impl; + +import dev.rubidium.subscriptiontool.model.UnsentMail; +import dev.rubidium.subscriptiontool.persistence.entity.Mail; +import dev.rubidium.subscriptiontool.persistence.entity.Subscription; +import dev.rubidium.subscriptiontool.persistence.repository.MailRepository; +import dev.rubidium.subscriptiontool.persistence.repository.SubscriptionRepository; +import dev.rubidium.subscriptiontool.service.PersistenceService; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class PersistenceServiceImpl implements PersistenceService { + + private static final Logger logger = LoggerFactory.getLogger(PersistenceServiceImpl.class); + + private final SubscriptionRepository subscriptionRepository; + private final MailRepository mailRepository; + + PersistenceServiceImpl(final SubscriptionRepository subscriptionRepository, + final MailRepository mailRepository) { + this.subscriptionRepository = subscriptionRepository; + this.mailRepository = mailRepository; + } + + @Override + public boolean createSubscription(String email, String code) { + Subscription registered = subscriptionRepository.findByMail(email); + if (registered == null) { + Subscription subscription = new Subscription(); + subscription.setMail(email); + subscription.setCode(code); + subscription.setConfirmed(Boolean.FALSE); + subscription.setRegistration(Timestamp.valueOf(LocalDateTime.now())); + subscriptionRepository.saveAndFlush(subscription); + + Long subscriptionId = subscriptionRepository.findByMail(email).getId(); + + Mail mail = new Mail(); + mail.setSubscription(subscriptionId); + mail.setSent(Boolean.FALSE); + mail.setCreation(Timestamp.valueOf(LocalDateTime.now())); + mailRepository.saveAndFlush(mail); + + return true; + } + logger.warn("Subscriber # {} tried to subscribe again", registered.getId()); + return false; + } + + @Override + public boolean deleteSubscription(String email) { + Subscription subscription = subscriptionRepository.findByMail(email); + if (subscription != null) { + Mail mail = mailRepository.findBySubscription(subscription.getId()); + if (mail != null) { + mailRepository.deleteById(mail.getId()); + } + subscriptionRepository.deleteById(subscription.getId()); + return true; + } + logger.warn("No subscription found for email {}", email); + return false; + } + + @Override + public boolean updateSubscription(String code) { + Subscription subscription = subscriptionRepository.findByCode(code); + if (subscription != null) { + subscription.setConfirmed(Boolean.TRUE); + subscription.setConfirmation(Timestamp.valueOf(LocalDateTime.now())); + subscriptionRepository.saveAndFlush(subscription); + return true; + } + logger.warn("No subscription found for code {}", code); + return false; + } + + @Override + public List<UnsentMail> getUnsentMails() { + var mails = new ArrayList<UnsentMail>(); + mailRepository.findUnsentMails().forEach(mail -> { + subscriptionRepository.findById(mail.getSubscription()).ifPresent(subscription -> + mails.add(new UnsentMail(mail.getId(), subscription.getMail(), subscription.getCode()))); + }); + return mails; + } + + @Override + public void updateMail(Long id) { + mailRepository.findById(id).ifPresent(mail -> { + mail.setSent(Boolean.TRUE); + mail.setCompletion(Timestamp.valueOf(LocalDateTime.now())); + mailRepository.saveAndFlush(mail); + }); + } +} diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/impl/SubscriptionServiceImpl.java b/src/main/java/dev/rubidium/subscriptiontool/service/impl/SubscriptionServiceImpl.java new file mode 100644 index 0000000..856ea18 --- /dev/null +++ b/src/main/java/dev/rubidium/subscriptiontool/service/impl/SubscriptionServiceImpl.java @@ -0,0 +1,43 @@ +package dev.rubidium.subscriptiontool.service.impl; + +import dev.rubidium.subscriptiontool.service.CodeService; +import dev.rubidium.subscriptiontool.service.PersistenceService; +import dev.rubidium.subscriptiontool.service.SubscriptionService; +import java.time.LocalDateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class SubscriptionServiceImpl implements SubscriptionService { + + private static final Logger logger = LoggerFactory.getLogger(SubscriptionServiceImpl.class); + + private final CodeService codeService; + private final PersistenceService persistenceService; + + public SubscriptionServiceImpl(final CodeService codeService, + final PersistenceService persistenceService) { + this.codeService = codeService; + this.persistenceService = persistenceService; + } + + @Override + public boolean saveSubscription(String email) { + logger.info("Saving a new subscription at {}", LocalDateTime.now()); + String code = codeService.generateCode(email); + return persistenceService.createSubscription(email, code); + } + + @Override + public boolean deleteSubscription(String email) { + logger.info("Deleting a subscription at {}", LocalDateTime.now()); + return persistenceService.deleteSubscription(email); + } + + @Override + public boolean confirmSubscription(String code) { + logger.info("Confirming a subscription at {}", LocalDateTime.now()); + return persistenceService.updateSubscription(code); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6af908c..778e402 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,13 +6,29 @@ info.app-author=Thorsten Ortlepp info.app-license=MIT License spring.datasource.url=jdbc:postgresql://localhost:5000/subscriptiontool -spring.datasource.username=postgres +spring.datasource.username=USERNAME spring.datasource.password=PASSWORD +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.properties.hibernate.default_schema=public +spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + +spring.mail.host=smtp.example.com +spring.mail.port=587 +spring.mail.username=USERNAME +spring.mail.password=PASSWORD +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=true + management.info.build.enabled=true management.info.env.enabled=true management.info.java.enabled=true management.info.os.enabled=true management.endpoints.web.exposure.include=health,info,metrics,flyway +subscriptiontool.mail.from=hello@example.com +subscriptiontool.mail.url=https://example.com/confirm?code= +subscriptiontool.scheduler.cron=0 * * * * * +subscriptiontool.scheduler.zone=Europe/Berlin + spring.config.import=translation.properties diff --git a/src/main/resources/db/migration/V1_0__create_tables.sql b/src/main/resources/db/migration/V1_0__create_tables.sql index 616fd1e..2f23b2c 100644 --- a/src/main/resources/db/migration/V1_0__create_tables.sql +++ b/src/main/resources/db/migration/V1_0__create_tables.sql @@ -1,7 +1,7 @@ CREATE TABLE subscriptions ( id BIGINT GENERATED ALWAYS AS IDENTITY UNIQUE, mail VARCHAR(255) NOT NULL UNIQUE, - code CHAR(64) NOT NULL, + code CHAR(32) NOT NULL UNIQUE, confirmed BOOLEAN NOT NULL DEFAULT FALSE, registration TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, confirmation TIMESTAMP diff --git a/src/main/resources/translation.properties b/src/main/resources/translation.properties index ef468fc..749deeb 100644 --- a/src/main/resources/translation.properties +++ b/src/main/resources/translation.properties @@ -12,3 +12,5 @@ translation.confirmSuccess=Your subscription has been confirmed. translation.confirmError=Error confirming your subscription. Please try again later. translation.emailPlaceholder=Your email address translation.back=Back +translation.mailSubject=Please confirm your subscription +translation.mailText=Hi,\n\nplease confirm your subscription by clicking the following link:\n%s\n\nBest regards diff --git a/src/test/java/dev/rubidium/subscriptiontool/service/CodeServiceTest.java b/src/test/java/dev/rubidium/subscriptiontool/service/CodeServiceTest.java new file mode 100644 index 0000000..54222f8 --- /dev/null +++ b/src/test/java/dev/rubidium/subscriptiontool/service/CodeServiceTest.java @@ -0,0 +1,27 @@ +package dev.rubidium.subscriptiontool.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import dev.rubidium.subscriptiontool.service.impl.CodeServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class CodeServiceTest { + + private CodeService codeService; + + @BeforeEach + void setUp() { + codeService = new CodeServiceImpl(); + } + + @Test + void testGenerateCode() { + final String input = "test@example.com"; + final String expected = "55502f40dc8b3c769880b10874abc9d0"; + + var result = codeService.generateCode(input); + + assertEquals(expected, result); + } +}