Erweiterungen

This commit is contained in:
2025-08-29 09:55:21 +02:00
parent 21b0bb946f
commit 4d2ed39275
7 changed files with 186 additions and 2 deletions

View File

@@ -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:

View File

@@ -103,6 +103,13 @@
<artifactId>spring-messaging</artifactId>
</dependency>
<!-- Zeroconf mDNS (JmDNS) -->
<dependency>
<groupId>org.jmdns</groupId>
<artifactId>jmdns</artifactId>
<version>3.6.1</version>
</dependency>
</dependencies>
<build>

View File

@@ -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);
}
}

View File

@@ -0,0 +1,9 @@
package de.assecutor.votianlt.dto;
import lombok.Data;
@Data
public class AppLoginRequest {
private String email;
private String password;
}

View File

@@ -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
}

View File

@@ -13,8 +13,9 @@ public interface AppUserRepository extends MongoRepository<AppUser, ObjectId> {
// Find all AppUsers created by a specific user
List<AppUser> 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<AppUser> findByEmail(String email);
// List<AppUser> findByBezeichnung(String bezeichnung);
}

View File

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