From 29e1599fcc8332cd5a9018ae2addd55546da6f58 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Tue, 16 Sep 2025 11:36:30 +0200 Subject: [PATCH] Erweiterungen --- pom.xml | 22 ++++++ .../votianlt/mqtt/MqttV5ClientManager.java | 75 +++++++++++++++++-- .../pages/base/ui/view/MainLayout.java | 2 +- .../pages/view/AuthenticatedStartView.java | 10 ++- .../votianlt/pages/view/LoginView.java | 16 +++- .../votianlt/repository/TaskRepository.java | 5 ++ 6 files changed, 121 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index b2c9c1a..e89cb17 100644 --- a/pom.xml +++ b/pom.xml @@ -103,6 +103,28 @@ openpdf 1.3.30 + + + + com.itextpdf + itext7-core + 7.2.5 + pom + + + + + com.itextpdf + kernel + 7.2.5 + + + + + com.itextpdf + layout + 7.2.5 + org.springframework.boot diff --git a/src/main/java/de/assecutor/votianlt/mqtt/MqttV5ClientManager.java b/src/main/java/de/assecutor/votianlt/mqtt/MqttV5ClientManager.java index a08cb2e..103815e 100644 --- a/src/main/java/de/assecutor/votianlt/mqtt/MqttV5ClientManager.java +++ b/src/main/java/de/assecutor/votianlt/mqtt/MqttV5ClientManager.java @@ -8,6 +8,8 @@ import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient; import com.hivemq.client.mqtt.mqtt5.Mqtt5Client; import de.assecutor.votianlt.config.MqttProperties; import de.assecutor.votianlt.controller.MessageController; +import de.assecutor.votianlt.model.PendingMqttMessage; +import de.assecutor.votianlt.repository.PendingMqttMessageRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.context.SmartLifecycle; import org.springframework.stereotype.Service; @@ -16,6 +18,7 @@ import org.springframework.context.annotation.Lazy; import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -29,14 +32,16 @@ public class MqttV5ClientManager implements SmartLifecycle { private final MqttProperties props; private final MessageController messageController; + private final PendingMqttMessageRepository pendingMessageRepository; private final ObjectMapper objectMapper = new ObjectMapper(); private volatile boolean running = false; private Mqtt5AsyncClient client; - public MqttV5ClientManager(MqttProperties props, @Lazy MessageController messageController) { + public MqttV5ClientManager(MqttProperties props, @Lazy MessageController messageController, PendingMqttMessageRepository pendingMessageRepository) { this.props = props; this.messageController = messageController; + this.pendingMessageRepository = pendingMessageRepository; } @Override @@ -89,6 +94,9 @@ public class MqttV5ClientManager implements SmartLifecycle { } running = true; log.info("[MQTT] Subscribed to {} topics (QoS={}), awaiting messages ...", topics.length, qos); + + // Process pending messages after successful connection + processPendingMessages(); } catch (Exception e) { log.error("Failed to start HiveMQ MQTT client: {}", e.getMessage(), e); } @@ -192,18 +200,75 @@ public class MqttV5ClientManager implements SmartLifecycle { public void publish(String topic, byte[] payload, int qos, boolean retained) { try { - if (client == null) { - log.warn("[MQTT] Not connected, dropping publish topic={}", topic); + if (client == null || !running) { + log.warn("[MQTT] Not connected, saving message for later: topic={}", topic); + savePendingMessage(topic, payload, qos, retained); return; } client.publishWith().topic(topic).payload(payload).qos(mapQos(qos)).retain(retained).send() .whenComplete((ack, ex) -> { if (ex != null) { - log.error("Failed to publish to {}: {}", topic, ex.getMessage(), ex); + log.error("Failed to publish to {}: {}, saving for retry", topic, ex.getMessage(), ex); + savePendingMessage(topic, payload, qos, retained); + } else { + log.debug("Successfully published to topic: {}", topic); } }); } catch (Exception e) { - log.error("Failed to publish to {}: {}", topic, e.getMessage(), e); + log.error("Failed to publish to {}: {}, saving for retry", topic, e.getMessage(), e); + savePendingMessage(topic, payload, qos, retained); + } + } + + private void savePendingMessage(String topic, byte[] payload, int qos, boolean retained) { + try { + PendingMqttMessage pendingMessage = new PendingMqttMessage(topic, payload, qos, retained); + pendingMessageRepository.save(pendingMessage); + log.info("[MQTT] Saved pending message for topic: {}", topic); + } catch (Exception e) { + log.error("Failed to save pending MQTT message: {}", e.getMessage(), e); + } + } + + private void processPendingMessages() { + try { + List pendingMessages = pendingMessageRepository.findAllByOrderByCreatedAtAsc(); + if (pendingMessages.isEmpty()) { + log.debug("[MQTT] No pending messages to process"); + return; + } + + log.info("[MQTT] Processing {} pending messages", pendingMessages.size()); + for (PendingMqttMessage pendingMessage : pendingMessages) { + try { + // Attempt to send the pending message + client.publishWith() + .topic(pendingMessage.getTopic()) + .payload(pendingMessage.getPayload()) + .qos(mapQos(pendingMessage.getQos())) + .retain(pendingMessage.isRetained()) + .send() + .whenComplete((ack, ex) -> { + if (ex != null) { + log.warn("Failed to resend pending message to {}: {}", pendingMessage.getTopic(), ex.getMessage()); + // Update retry count + pendingMessage.incrementRetryCount(); + pendingMessageRepository.save(pendingMessage); + } else { + // Successfully sent, remove from pending + log.info("Successfully resent pending message to topic: {}", pendingMessage.getTopic()); + pendingMessageRepository.delete(pendingMessage); + } + }).join(); // Wait for completion to process sequentially + + } catch (Exception e) { + log.warn("Error processing pending message for {}: {}", pendingMessage.getTopic(), e.getMessage()); + pendingMessage.incrementRetryCount(); + pendingMessageRepository.save(pendingMessage); + } + } + } catch (Exception e) { + log.error("Error processing pending MQTT messages: {}", e.getMessage(), e); } } } diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java index 341705d..ea2be4e 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java @@ -27,7 +27,7 @@ import static com.vaadin.flow.theme.lumo.LumoUtility.*; @AnonymousAllowed -@Layout +@Layout("main") public final class MainLayout extends AppLayout { private final SecurityService securityService; diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java b/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java index 0b226ce..7e3d523 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java @@ -12,16 +12,24 @@ import com.vaadin.flow.router.Route; import de.assecutor.votianlt.security.SecurityService; import de.assecutor.votianlt.pages.base.ui.view.MainLayout; import jakarta.annotation.security.RolesAllowed; +import com.vaadin.flow.component.UI; @Route(value = "dashboard", layout = MainLayout.class) @PageTitle("VotianLT - Dashboard") -@RolesAllowed("USER") +@RolesAllowed({"USER"}) public class AuthenticatedStartView extends VerticalLayout { private final SecurityService securityService; public AuthenticatedStartView(SecurityService securityService) { this.securityService = securityService; + + // Redirect admin users to admin dashboard + if (securityService.hasRole("ADMIN")) { + getUI().ifPresent(ui -> ui.navigate("admin-dashboard")); + return; + } + setSizeFull(); setPadding(false); setSpacing(false); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java b/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java index 0295ed7..780ccd2 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java @@ -115,7 +115,13 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af wrappedSession.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext()); } - UI.getCurrent().getPage().setLocation("/dashboard"); + // Check if user is admin and redirect accordingly + if (auth.getAuthorities().stream() + .anyMatch(authority -> authority.getAuthority().equals("ROLE_ADMIN"))) { + UI.getCurrent().getPage().setLocation("/admin-dashboard"); + } else { + UI.getCurrent().getPage().setLocation("/dashboard"); + } } } catch (Exception ex) { loginForm.setError(true); @@ -152,7 +158,13 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af } this.pendingAuth = null; // Full reload, damit der neue SecurityContext im UI sicher greift - UI.getCurrent().getPage().setLocation("/dashboard"); + // Check if user is admin and redirect accordingly + if (SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream() + .anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"))) { + UI.getCurrent().getPage().setLocation("/admin-dashboard"); + } else { + UI.getCurrent().getPage().setLocation("/dashboard"); + } } @Override diff --git a/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java b/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java index c6a8b09..3587e12 100644 --- a/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java +++ b/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java @@ -9,6 +9,11 @@ import java.util.List; public interface TaskRepository extends MongoRepository { List findByJobIdOrderByTaskOrderAsc(ObjectId jobId); + /** + * Count tasks by completion status + */ + long countByCompleted(boolean completed); + // Deprecated - use findByJobIdOrderByTaskOrderAsc instead @Deprecated default List findByJobId(ObjectId jobId) {