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) { + } + } + } +}