diff --git a/STOMP_README.md b/STOMP_README.md
index d327e46..3f874b3 100644
--- a/STOMP_README.md
+++ b/STOMP_README.md
@@ -17,6 +17,7 @@ Das System bietet folgende STOMP-Funktionalitäten:
- **`/app/message`** - Allgemeine Nachrichten
- **`/app/job/status`** - Job-Status-Updates
- **`/app/device/location`** - Gerätestandort-Updates
+- **`/app/auth/login`** - Anmeldung eines App-Users (Payload: { email, password })
#### Ausgehende Nachrichten (Server → Client)
- **`/topic/messages`** - Broadcast aller allgemeinen Nachrichten
@@ -75,6 +76,19 @@ stompClient.send('/app/device/location', {}, JSON.stringify({
longitude: 13.4050,
accuracy: 10
}));
+
+// Anmeldung eines App-Users
+// Zuerst die Antwort-Warteschlange abonnieren (user-spezifisch)
+const authSubscription = stompClient.subscribe('/user/queue/auth', function(message) {
+ const resp = JSON.parse(message.body);
+ console.log('Login-Antwort:', resp);
+});
+
+// Login-Request senden
+stompClient.send('/app/auth/login', {}, JSON.stringify({
+ email: 'user@example.com',
+ password: 'geheimesPasswort'
+}));
```
## Backend-Integration
@@ -92,6 +106,23 @@ messageController.sendNotificationToUser("username", "Neue Aufgabe verfügbar");
messageController.sendBroadcastMessage("Systemwartung in 10 Minuten");
```
+## Zeroconf (mDNS) Veröffentlichung
+
+Die Anwendung veröffentlicht die STOMP-Schnittstelle via Zeroconf (DNS-SD/mDNS), sofern verfügbar. Es wird der Service-Typ `_stomp._tcp.local.` mit folgenden TXT-Records publiziert:
+- path = Pfad für SockJS-Endpoint (Standard: /ws)
+- websocket = Pfad für nativen WebSocket (Standard: /websocket)
+- protocol = "stomp"
+
+Clients können per Bonjour/mDNS nach `_stomp._tcp` suchen und erhalten Port und Metadaten.
+
+Hinweise:
+- Die Implementierung nutzt JmDNS, falls die Bibliothek auf dem Klassenpfad vorhanden ist. In Umgebungen ohne JmDNS bleibt Zeroconf stillschweigend deaktiviert (es wird ein Hinweis im Log ausgegeben).
+- Konfigurierbare Properties:
+ - app.zeroconf.enabled (default: true)
+ - app.zeroconf.serviceName (default: votianlt-stomp)
+ - app.stomp.wsPath (default: /ws)
+ - app.stomp.websocketPath (default: /websocket)
+
## Konfiguration
Die STOMP-Konfiguration befindet sich in:
diff --git a/pom.xml b/pom.xml
index 5e9a74f..f4f9113 100644
--- a/pom.xml
+++ b/pom.xml
@@ -103,6 +103,13 @@
spring-messaging
+
+
+ org.jmdns
+ jmdns
+ 3.6.1
+
+
diff --git a/src/main/java/de/assecutor/votianlt/controller/MessageController.java b/src/main/java/de/assecutor/votianlt/controller/MessageController.java
index 44b6228..6246fd6 100644
--- a/src/main/java/de/assecutor/votianlt/controller/MessageController.java
+++ b/src/main/java/de/assecutor/votianlt/controller/MessageController.java
@@ -1,8 +1,14 @@
package de.assecutor.votianlt.controller;
+import de.assecutor.votianlt.dto.AppLoginRequest;
+import de.assecutor.votianlt.dto.AppLoginResponse;
+import de.assecutor.votianlt.model.AppUser;
+import de.assecutor.votianlt.pages.service.AppUserService;
+import de.assecutor.votianlt.repository.AppUserRepository;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
+import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.stereotype.Controller;
import org.springframework.beans.factory.annotation.Autowired;
@@ -20,6 +26,12 @@ public class MessageController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
+ @Autowired
+ private AppUserRepository appUserRepository;
+
+ @Autowired
+ private AppUserService appUserService;
+
/**
* Handles messages sent to /app/message and broadcasts them to all subscribers of /topic/messages
*/
@@ -82,4 +94,30 @@ public class MessageController {
messagingTemplate.convertAndSend("/topic/broadcasts", broadcast);
}
+
+ /**
+ * Authentication endpoint for mobile app users via STOMP.
+ * Client sends to /app/auth/login with payload { email, password }.
+ * The response is sent back to the requesting user on /user/queue/auth
+ */
+ @MessageMapping("/auth/login")
+ @SendToUser("/queue/auth")
+ public AppLoginResponse handleAppLogin(AppLoginRequest request) {
+ if (request == null || request.getEmail() == null || request.getPassword() == null
+ || request.getEmail().isBlank() || request.getPassword().isBlank()) {
+ return new AppLoginResponse(false, "E-Mail und Passwort sind erforderlich", null);
+ }
+
+ AppUser user = appUserRepository.findByEmail(request.getEmail());
+ if (user == null) {
+ return new AppLoginResponse(false, "Benutzer nicht gefunden", null);
+ }
+
+ boolean ok = appUserService.verifyPassword(request.getPassword(), user.getPassword());
+ if (!ok) {
+ return new AppLoginResponse(false, "Ungültige Anmeldedaten", null);
+ }
+
+ return new AppLoginResponse(true, "Anmeldung erfolgreich", user.getId() != null ? user.getId().toHexString() : null);
+ }
}
\ No newline at end of file
diff --git a/src/main/java/de/assecutor/votianlt/dto/AppLoginRequest.java b/src/main/java/de/assecutor/votianlt/dto/AppLoginRequest.java
new file mode 100644
index 0000000..b287166
--- /dev/null
+++ b/src/main/java/de/assecutor/votianlt/dto/AppLoginRequest.java
@@ -0,0 +1,9 @@
+package de.assecutor.votianlt.dto;
+
+import lombok.Data;
+
+@Data
+public class AppLoginRequest {
+ private String email;
+ private String password;
+}
diff --git a/src/main/java/de/assecutor/votianlt/dto/AppLoginResponse.java b/src/main/java/de/assecutor/votianlt/dto/AppLoginResponse.java
new file mode 100644
index 0000000..0691612
--- /dev/null
+++ b/src/main/java/de/assecutor/votianlt/dto/AppLoginResponse.java
@@ -0,0 +1,12 @@
+package de.assecutor.votianlt.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class AppLoginResponse {
+ private boolean success;
+ private String message;
+ private String appUserId; // MongoDB ObjectId as hex string
+}
diff --git a/src/main/java/de/assecutor/votianlt/repository/AppUserRepository.java b/src/main/java/de/assecutor/votianlt/repository/AppUserRepository.java
index 6bbe3c4..93add48 100644
--- a/src/main/java/de/assecutor/votianlt/repository/AppUserRepository.java
+++ b/src/main/java/de/assecutor/votianlt/repository/AppUserRepository.java
@@ -13,8 +13,9 @@ public interface AppUserRepository extends MongoRepository {
// Find all AppUsers created by a specific user
List findByErstelltVon(ObjectId erstelltVon);
+ // Find AppUser by email for login
+ AppUser findByEmail(String email);
+
// Custom query methods can be added here if needed
- // For example:
- // List findByEmail(String email);
// List findByBezeichnung(String bezeichnung);
}
diff --git a/src/main/java/de/assecutor/votianlt/zeroconf/ZeroconfPublisher.java b/src/main/java/de/assecutor/votianlt/zeroconf/ZeroconfPublisher.java
new file mode 100644
index 0000000..879553f
--- /dev/null
+++ b/src/main/java/de/assecutor/votianlt/zeroconf/ZeroconfPublisher.java
@@ -0,0 +1,86 @@
+package de.assecutor.votianlt.zeroconf;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.event.ContextClosedEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.net.InetAddress;
+
+/**
+ * Publishes the STOMP WebSocket endpoint via Zeroconf (mDNS/Bonjour) using reflection.
+ * If JmDNS is present on the classpath, it will register the service _stomp._tcp.local.
+ */
+@Component
+public class ZeroconfPublisher {
+
+ private static final Logger logger = LoggerFactory.getLogger(ZeroconfPublisher.class);
+
+ @Value("${server.port:8080}")
+ private int serverPort;
+
+ // Expose stomp endpoints paths via TXT records
+ @Value("${app.stomp.wsPath:/ws}")
+ private String wsPath;
+
+ @Value("${app.stomp.websocketPath:/websocket}")
+ private String websocketPath;
+
+ @Value("${app.zeroconf.enabled:true}")
+ private boolean enabled;
+
+ @Value("${app.zeroconf.serviceName:votianlt-stomp}")
+ private String serviceName;
+
+ // Controls whether to log a notice if JmDNS is not available
+ @Value("${app.zeroconf.warnWhenMissing:false}")
+ private boolean warnWhenMissing;
+
+ private Object jmdns; // javax.jmdns.JmDNS instance if available
+
+ @EventListener(org.springframework.boot.context.event.ApplicationReadyEvent.class)
+ public void onAppReady() {
+ if (!enabled) return;
+ try {
+ Class> jmDNSClass = Class.forName("javax.jmdns.JmDNS");
+ Class> serviceInfoClass = Class.forName("javax.jmdns.ServiceInfo");
+
+ InetAddress addr = InetAddress.getLocalHost();
+ Method createMethod = jmDNSClass.getMethod("create", InetAddress.class);
+ jmdns = createMethod.invoke(null, addr);
+
+ String type = "_stomp._tcp.local.";
+ String text = "path=" + wsPath + ",websocket=" + websocketPath + ",protocol=stomp";
+
+ Method createServiceInfo = serviceInfoClass.getMethod("create", String.class, String.class, int.class, String.class);
+ Object serviceInfo = createServiceInfo.invoke(null, type, serviceName, serverPort, text);
+
+ Method registerService = jmDNSClass.getMethod("registerService", serviceInfoClass);
+ registerService.invoke(jmdns, serviceInfo);
+
+ logger.info("STOMP-Service veröffentlicht: {} name={} port={}", type, serviceName, serverPort);
+ } catch (ClassNotFoundException e) {
+ if (warnWhenMissing) {
+ logger.warn("Hinweis: JmDNS ist nicht vorhanden – Zeroconf ist deaktiviert.");
+ }
+ } catch (Exception e) {
+ logger.error("Registrierung fehlgeschlagen: {}", e.getMessage(), e);
+ }
+ }
+
+ @EventListener(ContextClosedEvent.class)
+ public void onShutdown() {
+ if (jmdns != null) {
+ try {
+ Method unregisterAll = jmdns.getClass().getMethod("unregisterAllServices");
+ unregisterAll.invoke(jmdns);
+ Method close = jmdns.getClass().getMethod("close");
+ close.invoke(jmdns);
+ } catch (Exception ignored) {
+ }
+ }
+ }
+}