Erweiterungen

This commit is contained in:
2025-09-16 11:36:30 +02:00
parent 9d0045e655
commit 29e1599fcc
6 changed files with 121 additions and 9 deletions

22
pom.xml
View File

@@ -103,6 +103,28 @@
<artifactId>openpdf</artifactId> <artifactId>openpdf</artifactId>
<version>1.3.30</version> <version>1.3.30</version>
</dependency> </dependency>
<!-- iText 7 Core -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>7.2.5</version>
<type>pom</type>
</dependency>
<!-- iText 7 Kernel -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>kernel</artifactId>
<version>7.2.5</version>
</dependency>
<!-- iText 7 Layout -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>layout</artifactId>
<version>7.2.5</version>
</dependency>
<!-- Spring Boot Mail Starter --> <!-- Spring Boot Mail Starter -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

View File

@@ -8,6 +8,8 @@ import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient;
import com.hivemq.client.mqtt.mqtt5.Mqtt5Client; import com.hivemq.client.mqtt.mqtt5.Mqtt5Client;
import de.assecutor.votianlt.config.MqttProperties; import de.assecutor.votianlt.config.MqttProperties;
import de.assecutor.votianlt.controller.MessageController; import de.assecutor.votianlt.controller.MessageController;
import de.assecutor.votianlt.model.PendingMqttMessage;
import de.assecutor.votianlt.repository.PendingMqttMessageRepository;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.SmartLifecycle; import org.springframework.context.SmartLifecycle;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -16,6 +18,7 @@ import org.springframework.context.annotation.Lazy;
import java.net.URI; import java.net.URI;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@@ -29,14 +32,16 @@ public class MqttV5ClientManager implements SmartLifecycle {
private final MqttProperties props; private final MqttProperties props;
private final MessageController messageController; private final MessageController messageController;
private final PendingMqttMessageRepository pendingMessageRepository;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
private volatile boolean running = false; private volatile boolean running = false;
private Mqtt5AsyncClient client; private Mqtt5AsyncClient client;
public MqttV5ClientManager(MqttProperties props, @Lazy MessageController messageController) { public MqttV5ClientManager(MqttProperties props, @Lazy MessageController messageController, PendingMqttMessageRepository pendingMessageRepository) {
this.props = props; this.props = props;
this.messageController = messageController; this.messageController = messageController;
this.pendingMessageRepository = pendingMessageRepository;
} }
@Override @Override
@@ -89,6 +94,9 @@ public class MqttV5ClientManager implements SmartLifecycle {
} }
running = true; running = true;
log.info("[MQTT] Subscribed to {} topics (QoS={}), awaiting messages ...", topics.length, qos); log.info("[MQTT] Subscribed to {} topics (QoS={}), awaiting messages ...", topics.length, qos);
// Process pending messages after successful connection
processPendingMessages();
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to start HiveMQ MQTT client: {}", e.getMessage(), 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) { public void publish(String topic, byte[] payload, int qos, boolean retained) {
try { try {
if (client == null) { if (client == null || !running) {
log.warn("[MQTT] Not connected, dropping publish topic={}", topic); log.warn("[MQTT] Not connected, saving message for later: topic={}", topic);
savePendingMessage(topic, payload, qos, retained);
return; return;
} }
client.publishWith().topic(topic).payload(payload).qos(mapQos(qos)).retain(retained).send() client.publishWith().topic(topic).payload(payload).qos(mapQos(qos)).retain(retained).send()
.whenComplete((ack, ex) -> { .whenComplete((ack, ex) -> {
if (ex != null) { 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) { } 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<PendingMqttMessage> 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);
} }
} }
} }

View File

@@ -27,7 +27,7 @@ import static com.vaadin.flow.theme.lumo.LumoUtility.*;
@AnonymousAllowed @AnonymousAllowed
@Layout @Layout("main")
public final class MainLayout extends AppLayout { public final class MainLayout extends AppLayout {
private final SecurityService securityService; private final SecurityService securityService;

View File

@@ -12,16 +12,24 @@ import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.security.SecurityService; import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.pages.base.ui.view.MainLayout; import de.assecutor.votianlt.pages.base.ui.view.MainLayout;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
import com.vaadin.flow.component.UI;
@Route(value = "dashboard", layout = MainLayout.class) @Route(value = "dashboard", layout = MainLayout.class)
@PageTitle("VotianLT - Dashboard") @PageTitle("VotianLT - Dashboard")
@RolesAllowed("USER") @RolesAllowed({"USER"})
public class AuthenticatedStartView extends VerticalLayout { public class AuthenticatedStartView extends VerticalLayout {
private final SecurityService securityService; private final SecurityService securityService;
public AuthenticatedStartView(SecurityService securityService) { public AuthenticatedStartView(SecurityService securityService) {
this.securityService = securityService; this.securityService = securityService;
// Redirect admin users to admin dashboard
if (securityService.hasRole("ADMIN")) {
getUI().ifPresent(ui -> ui.navigate("admin-dashboard"));
return;
}
setSizeFull(); setSizeFull();
setPadding(false); setPadding(false);
setSpacing(false); setSpacing(false);

View File

@@ -115,7 +115,13 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
wrappedSession.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, wrappedSession.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
SecurityContextHolder.getContext()); 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) { } catch (Exception ex) {
loginForm.setError(true); loginForm.setError(true);
@@ -152,7 +158,13 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
} }
this.pendingAuth = null; this.pendingAuth = null;
// Full reload, damit der neue SecurityContext im UI sicher greift // 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 @Override

View File

@@ -9,6 +9,11 @@ import java.util.List;
public interface TaskRepository extends MongoRepository<BaseTask, ObjectId> { public interface TaskRepository extends MongoRepository<BaseTask, ObjectId> {
List<BaseTask> findByJobIdOrderByTaskOrderAsc(ObjectId jobId); List<BaseTask> findByJobIdOrderByTaskOrderAsc(ObjectId jobId);
/**
* Count tasks by completion status
*/
long countByCompleted(boolean completed);
// Deprecated - use findByJobIdOrderByTaskOrderAsc instead // Deprecated - use findByJobIdOrderByTaskOrderAsc instead
@Deprecated @Deprecated
default List<BaseTask> findByJobId(ObjectId jobId) { default List<BaseTask> findByJobId(ObjectId jobId) {