git » subscription-tool.git » commit 17ff7dd

added management features

author Thorsten Ortlepp
2026-01-30 23:10:23 UTC
committer Thorsten Ortlepp
2026-01-30 23:10:23 UTC
parent 136fe7269a95dfe31987e5bcf710bce1620b00c2

added management features

src/main/java/dev/rubidium/subscriptiontool/SubscriptionToolApplication.java +0 -2
src/main/java/dev/rubidium/subscriptiontool/configuration/SecurityConfiguration.java +1 -1
src/main/java/dev/rubidium/subscriptiontool/controller/ManagementController.java +69 -0
src/main/java/dev/rubidium/subscriptiontool/model/Action.java +6 -0
src/main/java/dev/rubidium/subscriptiontool/model/ManagementAction.java +23 -0
src/main/java/dev/rubidium/subscriptiontool/model/Subscriber.java +13 -0
src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Mail.java +15 -11
src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Subscription.java +13 -0
src/main/java/dev/rubidium/subscriptiontool/persistence/repository/SubscriptionRepository.java +4 -0
src/main/java/dev/rubidium/subscriptiontool/properties/Translation.java +13 -1
src/main/java/dev/rubidium/subscriptiontool/scheduler/MailScheduler.java +2 -0
src/main/java/dev/rubidium/subscriptiontool/service/ManagementService.java +13 -0
src/main/java/dev/rubidium/subscriptiontool/service/PersistenceService.java +8 -1
src/main/java/dev/rubidium/subscriptiontool/service/impl/ManagementServiceImpl.java +48 -0
src/main/java/dev/rubidium/subscriptiontool/service/impl/PersistenceServiceImpl.java +77 -23
src/main/java/dev/rubidium/subscriptiontool/service/impl/SubscriptionServiceImpl.java +1 -1
src/main/resources/application.properties +2 -0
src/main/resources/templates/Manage.html +75 -0
src/main/resources/translation.properties +13 -1

diff --git a/src/main/java/dev/rubidium/subscriptiontool/SubscriptionToolApplication.java b/src/main/java/dev/rubidium/subscriptiontool/SubscriptionToolApplication.java
index 2720e0b..c782084 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/SubscriptionToolApplication.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/SubscriptionToolApplication.java
@@ -5,11 +5,9 @@ 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, MailProperties.class})
-@EnableScheduling
 public class SubscriptionToolApplication {
 
   static void main(String[] args) {
diff --git a/src/main/java/dev/rubidium/subscriptiontool/configuration/SecurityConfiguration.java b/src/main/java/dev/rubidium/subscriptiontool/configuration/SecurityConfiguration.java
index 1af5023..ebbf3ec 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/configuration/SecurityConfiguration.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/configuration/SecurityConfiguration.java
@@ -28,7 +28,7 @@ public class SecurityConfiguration {
   public SecurityFilterChain defaultHttpSecurity(HttpSecurity httpSecurity) {
     httpSecurity
         .authorizeHttpRequests((requests) -> requests
-            .requestMatchers("/actuator", "/actuator/**")
+            .requestMatchers("/actuator", "/actuator/**", "/manage", "/manage/**")
             .authenticated()
             .anyRequest()
             .permitAll())
diff --git a/src/main/java/dev/rubidium/subscriptiontool/controller/ManagementController.java b/src/main/java/dev/rubidium/subscriptiontool/controller/ManagementController.java
new file mode 100644
index 0000000..d952ea2
--- /dev/null
+++ b/src/main/java/dev/rubidium/subscriptiontool/controller/ManagementController.java
@@ -0,0 +1,69 @@
+package dev.rubidium.subscriptiontool.controller;
+
+import dev.rubidium.subscriptiontool.model.Action;
+import dev.rubidium.subscriptiontool.model.ManagementAction;
+import dev.rubidium.subscriptiontool.model.Subscriber;
+import dev.rubidium.subscriptiontool.properties.Translation;
+import dev.rubidium.subscriptiontool.service.ManagementService;
+import java.util.ArrayList;
+import java.util.List;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PostMapping;
+
+@Controller
+public class ManagementController {
+
+  private static final String ATTRIBUTE_NAME_TRANSLATION = "translation";
+  private static final String ATTRIBUTE_NAME_SUBSCRIBERS = "subscribers";
+  private static final String ATTRIBUTE_NAME_MAILS = "mails";
+
+  private final ManagementService managementService;
+  private final Translation translation;
+
+  public ManagementController(final ManagementService managementService,
+      final Translation translation) {
+    this.managementService = managementService;
+    this.translation = translation;
+  }
+
+  @GetMapping("/manage")
+  public String index(Model model) {
+    var subscribers = managementService.getSubscribers();
+
+    model.addAttribute(ATTRIBUTE_NAME_TRANSLATION, translation);
+    model.addAttribute(ATTRIBUTE_NAME_SUBSCRIBERS, subscribers);
+    model.addAttribute(ATTRIBUTE_NAME_MAILS, extractMails(subscribers));
+
+    return "Manage";
+  }
+
+  @PostMapping("/manage")
+  public String action(@ModelAttribute ManagementAction managementAction, Model model) {
+    if (managementAction.getAction() == Action.ACTIVATE) {
+      managementService.activateSubscription(managementAction.getSubscription());
+    } else if (managementAction.getAction() == Action.DELETE) {
+      managementService.deleteSubscription(managementAction.getSubscription());
+    }
+
+    var subscribers = managementService.getSubscribers();
+
+    model.addAttribute(ATTRIBUTE_NAME_TRANSLATION, translation);
+    model.addAttribute(ATTRIBUTE_NAME_SUBSCRIBERS, subscribers);
+    model.addAttribute(ATTRIBUTE_NAME_MAILS, extractMails(subscribers));
+
+    return "Manage";
+  }
+
+  private String extractMails(List<Subscriber> subscribers) {
+    var mails = new ArrayList<String>();
+    subscribers.forEach(subscriber -> {
+      if (subscriber.confirmed()) {
+        mails.add(subscriber.mail());
+      }
+    });
+    return String.join(", ", mails);
+  }
+}
diff --git a/src/main/java/dev/rubidium/subscriptiontool/model/Action.java b/src/main/java/dev/rubidium/subscriptiontool/model/Action.java
new file mode 100644
index 0000000..a16d9c3
--- /dev/null
+++ b/src/main/java/dev/rubidium/subscriptiontool/model/Action.java
@@ -0,0 +1,6 @@
+package dev.rubidium.subscriptiontool.model;
+
+public enum Action {
+  ACTIVATE,
+  DELETE
+}
diff --git a/src/main/java/dev/rubidium/subscriptiontool/model/ManagementAction.java b/src/main/java/dev/rubidium/subscriptiontool/model/ManagementAction.java
new file mode 100644
index 0000000..ada85fd
--- /dev/null
+++ b/src/main/java/dev/rubidium/subscriptiontool/model/ManagementAction.java
@@ -0,0 +1,23 @@
+package dev.rubidium.subscriptiontool.model;
+
+public class ManagementAction {
+
+  private Action action;
+  private Long subscription;
+
+  public Action getAction() {
+    return action;
+  }
+
+  public void setAction(Action action) {
+    this.action = action;
+  }
+
+  public Long getSubscription() {
+    return subscription;
+  }
+
+  public void setSubscription(Long subscription) {
+    this.subscription = subscription;
+  }
+}
diff --git a/src/main/java/dev/rubidium/subscriptiontool/model/Subscriber.java b/src/main/java/dev/rubidium/subscriptiontool/model/Subscriber.java
new file mode 100644
index 0000000..1ef2c42
--- /dev/null
+++ b/src/main/java/dev/rubidium/subscriptiontool/model/Subscriber.java
@@ -0,0 +1,13 @@
+package dev.rubidium.subscriptiontool.model;
+
+import java.time.LocalDateTime;
+
+public record Subscriber(Long id,
+                         String mail,
+                         Boolean confirmed,
+                         LocalDateTime registration,
+                         LocalDateTime confirmation,
+                         Boolean doiMailStatus,
+                         LocalDateTime doiMailTime) {
+
+}
diff --git a/src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Mail.java b/src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Mail.java
index 4f90e39..cc819f8 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Mail.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Mail.java
@@ -5,6 +5,8 @@ import jakarta.persistence.Entity;
 import jakarta.persistence.GeneratedValue;
 import jakarta.persistence.GenerationType;
 import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.OneToOne;
 import jakarta.persistence.Table;
 import java.sql.Timestamp;
 
@@ -17,9 +19,6 @@ public class Mail {
   @Column(name = "id")
   private Long id;
 
-  @Column(name = "subscription")
-  private Long subscription;
-
   @Column(name = "sent")
   private Boolean sent;
 
@@ -29,6 +28,10 @@ public class Mail {
   @Column(name = "completion")
   private Timestamp completion;
 
+  @OneToOne
+  @JoinColumn(name = "subscription", referencedColumnName = "id")
+  private Subscription subscription;
+
   public Long getId() {
     return id;
   }
@@ -37,14 +40,6 @@ public class Mail {
     this.id = id;
   }
 
-  public Long getSubscription() {
-    return subscription;
-  }
-
-  public void setSubscription(Long subscription) {
-    this.subscription = subscription;
-  }
-
   public Boolean getSent() {
     return sent;
   }
@@ -68,4 +63,13 @@ public class Mail {
   public void setCompletion(Timestamp completion) {
     this.completion = completion;
   }
+
+  public Subscription getSubscription() {
+    return subscription;
+  }
+
+  public void setSubscription(
+      Subscription subscription) {
+    this.subscription = subscription;
+  }
 }
diff --git a/src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Subscription.java b/src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Subscription.java
index e300d56..acee97b 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Subscription.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/persistence/entity/Subscription.java
@@ -1,10 +1,12 @@
 package dev.rubidium.subscriptiontool.persistence.entity;
 
+import jakarta.persistence.CascadeType;
 import jakarta.persistence.Column;
 import jakarta.persistence.Entity;
 import jakarta.persistence.GeneratedValue;
 import jakarta.persistence.GenerationType;
 import jakarta.persistence.Id;
+import jakarta.persistence.OneToOne;
 import jakarta.persistence.Table;
 import java.sql.Timestamp;
 
@@ -32,6 +34,9 @@ public class Subscription {
   @Column(name = "confirmation")
   private Timestamp confirmation;
 
+  @OneToOne(mappedBy = "subscription", cascade = CascadeType.ALL, orphanRemoval = true)
+  private Mail relationMail;
+
   public Long getId() {
     return id;
   }
@@ -79,4 +84,12 @@ public class Subscription {
   public void setConfirmation(Timestamp confirmation) {
     this.confirmation = confirmation;
   }
+
+  public Mail getRelationMail() {
+    return relationMail;
+  }
+
+  public void setRelationMail(Mail relationMail) {
+    this.relationMail = relationMail;
+  }
 }
diff --git a/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/SubscriptionRepository.java b/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/SubscriptionRepository.java
index 51768c9..60e281c 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/SubscriptionRepository.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/SubscriptionRepository.java
@@ -1,6 +1,7 @@
 package dev.rubidium.subscriptiontool.persistence.repository;
 
 import dev.rubidium.subscriptiontool.persistence.entity.Subscription;
+import java.util.List;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
 
@@ -11,4 +12,7 @@ public interface SubscriptionRepository extends JpaRepository<Subscription, Long
 
   @Query("SELECT s FROM Subscription s WHERE s.code = :code")
   Subscription findByCode(String code);
+
+  @Query("SELECT s FROM Subscription s LEFT JOIN s.relationMail ORDER BY s.id ASC")
+  List<Subscription> findAllSubscriptions();
 }
diff --git a/src/main/java/dev/rubidium/subscriptiontool/properties/Translation.java b/src/main/java/dev/rubidium/subscriptiontool/properties/Translation.java
index 1f60f64..eca69dc 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/properties/Translation.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/properties/Translation.java
@@ -20,6 +20,18 @@ public record Translation(String title,
                           String mailSubject,
                           String mailText,
                           String error,
-                          String home) {
+                          String home,
+                          String manage,
+                          String tableMail,
+                          String tableStatus,
+                          String tableRegistered,
+                          String tableConfirmed,
+                          String tableDoiMail,
+                          String tableActions,
+                          String active,
+                          String inactive,
+                          String activate,
+                          String delete,
+                          String copy) {
 
 }
diff --git a/src/main/java/dev/rubidium/subscriptiontool/scheduler/MailScheduler.java b/src/main/java/dev/rubidium/subscriptiontool/scheduler/MailScheduler.java
index fa2c4cc..c46d590 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/scheduler/MailScheduler.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/scheduler/MailScheduler.java
@@ -6,10 +6,12 @@ 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.EnableScheduling;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 
 @Service
+@EnableScheduling
 public class MailScheduler {
 
   private static final Logger logger = LoggerFactory.getLogger(MailScheduler.class);
diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/ManagementService.java b/src/main/java/dev/rubidium/subscriptiontool/service/ManagementService.java
new file mode 100644
index 0000000..f9deefe
--- /dev/null
+++ b/src/main/java/dev/rubidium/subscriptiontool/service/ManagementService.java
@@ -0,0 +1,13 @@
+package dev.rubidium.subscriptiontool.service;
+
+import dev.rubidium.subscriptiontool.model.Subscriber;
+import java.util.List;
+
+public interface ManagementService {
+
+  List<Subscriber> getSubscribers();
+
+  void activateSubscription(Long id);
+
+  void deleteSubscription(Long id);
+}
diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/PersistenceService.java b/src/main/java/dev/rubidium/subscriptiontool/service/PersistenceService.java
index 1e6890b..f84896e 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/service/PersistenceService.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/service/PersistenceService.java
@@ -1,5 +1,6 @@
 package dev.rubidium.subscriptiontool.service;
 
+import dev.rubidium.subscriptiontool.model.Subscriber;
 import dev.rubidium.subscriptiontool.model.UnsentMail;
 import java.util.List;
 
@@ -7,11 +8,17 @@ public interface PersistenceService {
 
   boolean createSubscription(String email, String code);
 
+  boolean deleteSubscription(Long id);
+
   boolean deleteSubscription(String email);
 
-  boolean updateSubscription(String code);
+  boolean confirmSubscription(Long id);
+
+  boolean confirmSubscription(String code);
 
   List<UnsentMail> getUnsentMails();
 
   void updateMail(Long id);
+
+  List<Subscriber> getSubscribers();
 }
diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/impl/ManagementServiceImpl.java b/src/main/java/dev/rubidium/subscriptiontool/service/impl/ManagementServiceImpl.java
new file mode 100644
index 0000000..416e0ed
--- /dev/null
+++ b/src/main/java/dev/rubidium/subscriptiontool/service/impl/ManagementServiceImpl.java
@@ -0,0 +1,48 @@
+package dev.rubidium.subscriptiontool.service.impl;
+
+import dev.rubidium.subscriptiontool.model.Subscriber;
+import dev.rubidium.subscriptiontool.service.ManagementService;
+import dev.rubidium.subscriptiontool.service.PersistenceService;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+@Service
+public class ManagementServiceImpl implements ManagementService {
+
+  private static final Logger logger = LoggerFactory.getLogger(ManagementServiceImpl.class);
+
+  private final PersistenceService persistenceService;
+
+  public ManagementServiceImpl(final PersistenceService persistenceService) {
+    this.persistenceService = persistenceService;
+  }
+
+  @Override
+  public List<Subscriber> getSubscribers() {
+    var result = persistenceService.getSubscribers();
+    logger.info("Retrieved {} subscribers", result.size());
+    return result;
+  }
+
+  @Override
+  public void deleteSubscription(Long id) {
+    boolean result = persistenceService.deleteSubscription(id);
+    if (result) {
+      logger.warn("Deletion of subscription {} successful", id);
+    } else {
+      logger.warn("Deletion of subscription {} unsuccessful", id);
+    }
+  }
+
+  @Override
+  public void activateSubscription(Long id) {
+    boolean result = persistenceService.confirmSubscription(id);
+    if (result) {
+      logger.warn("Activation of subscription {} successful", id);
+    } else {
+      logger.warn("Activation of subscription {} unsuccessful", id);
+    }
+  }
+}
diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/impl/PersistenceServiceImpl.java b/src/main/java/dev/rubidium/subscriptiontool/service/impl/PersistenceServiceImpl.java
index d3f7e9d..cf5ae20 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/service/impl/PersistenceServiceImpl.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/service/impl/PersistenceServiceImpl.java
@@ -1,5 +1,6 @@
 package dev.rubidium.subscriptiontool.service.impl;
 
+import dev.rubidium.subscriptiontool.model.Subscriber;
 import dev.rubidium.subscriptiontool.model.UnsentMail;
 import dev.rubidium.subscriptiontool.persistence.entity.Mail;
 import dev.rubidium.subscriptiontool.persistence.entity.Subscription;
@@ -10,6 +11,7 @@ import java.sql.Timestamp;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
@@ -39,10 +41,8 @@ public class PersistenceServiceImpl implements PersistenceService {
       subscription.setRegistration(Timestamp.valueOf(LocalDateTime.now()));
       subscriptionRepository.saveAndFlush(subscription);
 
-      Long subscriptionId = subscriptionRepository.findByMail(email).getId();
-
       Mail mail = new Mail();
-      mail.setSubscription(subscriptionId);
+      mail.setSubscription(subscription);
       mail.setSent(Boolean.FALSE);
       mail.setCreation(Timestamp.valueOf(LocalDateTime.now()));
       mailRepository.saveAndFlush(mail);
@@ -53,45 +53,73 @@ public class PersistenceServiceImpl implements PersistenceService {
     return false;
   }
 
+  @Override
+  public boolean deleteSubscription(Long id) {
+    Subscription subscription = subscriptionRepository.findById(id).orElse(null);
+    boolean result = deleteSubscription(subscription);
+    if (!result) {
+      logger.warn("No subscription found for id {}", id);
+    }
+    return result;
+  }
+
   @Override
   public boolean deleteSubscription(String email) {
     Subscription subscription = subscriptionRepository.findByMail(email);
+    boolean result = deleteSubscription(subscription);
+    if (!result) {
+      logger.warn("No subscription found for email {}", email);
+    }
+    return result;
+  }
+
+  private boolean deleteSubscription(Subscription subscription) {
     if (subscription != null) {
-      Mail mail = mailRepository.findBySubscription(subscription.getId());
-      if (mail != null) {
-        mailRepository.deleteById(mail.getId());
+      if (subscription.getRelationMail() != null) {
+        mailRepository.deleteById(subscription.getRelationMail().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) {
-      if (subscription.getConfirmed()) {
-        logger.warn("Subscription {} already confirmed", subscription.getId());
-      } else {
-        subscription.setConfirmed(Boolean.TRUE);
-        subscription.setConfirmation(Timestamp.valueOf(LocalDateTime.now()));
-        subscriptionRepository.saveAndFlush(subscription);
-      }
-      return true;
+  public boolean confirmSubscription(Long id) {
+    Optional<Subscription> subscription = subscriptionRepository.findById(id);
+    if (subscription.isPresent()) {
+      return confirmSubscription(subscription.get());
     }
-    logger.warn("No subscription found for code {}", code);
+    logger.warn("No subscription found for id {}", id);
     return false;
   }
 
+  @Override
+  public boolean confirmSubscription(String code) {
+    Subscription subscription = subscriptionRepository.findByCode(code);
+    if (subscription == null) {
+      logger.warn("No subscription found for code {}", code);
+      return false;
+    }
+    return confirmSubscription(subscription);
+  }
+
+  private boolean confirmSubscription(Subscription subscription) {
+    if (subscription.getConfirmed()) {
+      logger.warn("Subscription {} already confirmed", subscription.getId());
+      return false;
+    }
+    subscription.setConfirmed(Boolean.TRUE);
+    subscription.setConfirmation(Timestamp.valueOf(LocalDateTime.now()));
+    subscriptionRepository.saveAndFlush(subscription);
+    return true;
+  }
+
   @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()))));
+    mailRepository.findUnsentMails().forEach(mail -> mails.add(new UnsentMail(mail.getId(),
+        mail.getSubscription().getMail(), mail.getSubscription().getCode())));
     return mails;
   }
 
@@ -103,4 +131,30 @@ public class PersistenceServiceImpl implements PersistenceService {
       mailRepository.saveAndFlush(mail);
     });
   }
+
+  @Override
+  public List<Subscriber> getSubscribers() {
+    var result = new ArrayList<Subscriber>();
+    subscriptionRepository.findAllSubscriptions().forEach(subscription -> {
+      Boolean doiMailStatus = Boolean.FALSE;
+      LocalDateTime doiMailTime = null;
+      if (subscription.getRelationMail() != null) {
+        doiMailStatus = subscription.getRelationMail().getSent();
+        if (subscription.getRelationMail().getCompletion() != null) {
+          doiMailTime = subscription.getRelationMail().getCompletion().toLocalDateTime();
+        }
+      }
+      LocalDateTime confirmation =
+          subscription.getConfirmation() == null ? null :
+              subscription.getConfirmation().toLocalDateTime();
+      result.add(new Subscriber(subscription.getId(),
+          subscription.getMail(),
+          subscription.getConfirmed(),
+          subscription.getRegistration().toLocalDateTime(),
+          confirmation,
+          doiMailStatus,
+          doiMailTime));
+    });
+    return result;
+  }
 }
diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/impl/SubscriptionServiceImpl.java b/src/main/java/dev/rubidium/subscriptiontool/service/impl/SubscriptionServiceImpl.java
index 856ea18..9fe52bc 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/service/impl/SubscriptionServiceImpl.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/service/impl/SubscriptionServiceImpl.java
@@ -38,6 +38,6 @@ public class SubscriptionServiceImpl implements SubscriptionService {
   @Override
   public boolean confirmSubscription(String code) {
     logger.info("Confirming a subscription at {}", LocalDateTime.now());
-    return persistenceService.updateSubscription(code);
+    return persistenceService.confirmSubscription(code);
   }
 }
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index b875956..cc8b1bc 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -8,6 +8,8 @@ info.app-license=MIT License
 spring.datasource.url=jdbc:postgresql://localhost:5000/subscriptiontool
 spring.datasource.username=USERNAME
 spring.datasource.password=PASSWORD
+spring.datasource.hikari.minimum-idle=3
+spring.datasource.hikari.maximum-pool-size=6
 
 spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
 spring.jpa.properties.hibernate.default_schema=public
diff --git a/src/main/resources/templates/Manage.html b/src/main/resources/templates/Manage.html
new file mode 100644
index 0000000..dd27564
--- /dev/null
+++ b/src/main/resources/templates/Manage.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org" lang="en">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <link href="/bootstrap.min.css" rel="stylesheet">
+  <title th:text="${translation.title()}"/>
+  <script>
+    function copyTo() {
+      var text = document.getElementById("mailto").textContent;
+      navigator.clipboard.writeText(text);
+    }
+  </script>
+</head>
+<body>
+<div class="container-fluid">
+  <h1 class="text-center fs-3"><span th:text="${translation.title()}"/> <span th:text="${translation.manage()}"/></h1>
+
+  <table class="table table-striped">
+    <thead>
+      <tr>
+        <th scope="col" th:text="${translation.tableMail()}"/>
+        <th scope="col" th:text="${translation.tableStatus()}"/>
+        <th scope="col" th:text="${translation.tableRegistered()}"/>
+        <th scope="col" th:text="${translation.tableConfirmed()}"/>
+        <th scope="col" th:text="${translation.tableDoiMail()}"/>
+        <th scope="col" colspan="2" th:text="${translation.tableActions()}"/>
+      </tr>
+    </thead>
+    <tbody>
+      <tr th:each="subscriber : ${subscribers}">
+        <td><span th:text="${subscriber.mail}"/></td>
+
+        <td th:if="${subscriber.confirmed == true}"><span th:text="${translation.active()}"/></td>
+        <td th:unless="${subscriber.confirmed == true}"><span th:text="${translation.inactive()}"/></td>
+
+        <td><span th:text="${#temporals.format(subscriber.registration)}"/></td>
+
+        <td th:if="${subscriber.confirmation == null}">&mdash;</td>
+        <td th:unless="${subscriber.confirmation == null}"><span th:text="${#temporals.format(subscriber.confirmation)}"/></td>
+
+        <td th:if="${subscriber.doiMailTime() == null}">&mdash;</td>
+        <td th:unless="${subscriber.doiMailTime() == null}"><span th:text="${#temporals.format(subscriber.doiMailTime())}"/></td>
+
+        <td>
+          <button type="button" class="btn btn-secondary btn-sm" disabled th:if="${subscriber.confirmed == true}" th:text="${translation.activate()}"/>
+          <form action="#" th:action="@{/manage}" method="post" th:unless="${subscriber.confirmed == true}">
+            <input type="hidden" name="action" th:value="${T(dev.rubidium.subscriptiontool.model.Action).ACTIVATE}"/>
+            <input type="hidden" name="subscription" th:value="${subscriber.id()}"/>
+            <input type="submit" class="btn btn-primary btn-sm" th:value="${translation.activate()}"/>
+          </form>
+        </td>
+        <td>
+          <form action="#" th:action="@{/manage}" method="post">
+            <input type="hidden" name="action" th:value="${T(dev.rubidium.subscriptiontool.model.Action).DELETE}"/>
+            <input type="hidden" name="subscription" th:value="${subscriber.id()}"/>
+            <input type="submit" class="btn btn-danger btn-sm" th:value="${translation.delete()}"/>
+          </form>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+
+  <div class="py-3">
+    <hr/>
+  </div>
+
+  <div class="py-3">
+    <pre class="bg-secondary text-white text-wrap lh-lg" id="mailto" th:text="${mails}"/>
+    <p><button class="btn btn-primary btn-sm" onclick="copyTo()" th:text="${translation.copy()}"/></p>
+  </div>
+
+</div>
+</body>
+</html>
diff --git a/src/main/resources/translation.properties b/src/main/resources/translation.properties
index 0ca249f..f9acb32 100644
--- a/src/main/resources/translation.properties
+++ b/src/main/resources/translation.properties
@@ -15,4 +15,16 @@ 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
 translation.error=An error occurred
-translation.home=Home
\ No newline at end of file
+translation.home=Home
+translation.manage=Management
+translation.tableMail=Mail
+translation.tableStatus=Status
+translation.tableRegistered=Registered
+translation.tableConfirmed=Confirmed
+translation.tableDoiMail=DOI Mail
+translation.tableActions=Actions
+translation.active=active
+translation.inactive=inactive
+translation.activate=activate
+translation.delete=delete
+translation.copy=copy to clipboard