From ca34aec0ba49a3de31cd93a3f56390c19e0fc26a Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Sun, 14 Sep 2025 12:09:13 +0200 Subject: [PATCH] MQTT --- STOMP_README.md | 177 ------------------ .../controller/MessageController.java | 54 +++--- .../votianlt/security/SecurityConfig.java | 2 +- src/main/resources/application.properties | 2 - 4 files changed, 28 insertions(+), 207 deletions(-) delete mode 100644 STOMP_README.md diff --git a/STOMP_README.md b/STOMP_README.md deleted file mode 100644 index 60671bd..0000000 --- a/STOMP_README.md +++ /dev/null @@ -1,177 +0,0 @@ -# VOTIANLT Realtime & Photo Upload API - -This document describes how mobile/Flutter apps can interact with the backend via STOMP (WebSocket) and how to upload photos via HTTP POST (new flow). - -## 1) STOMP Overview - -- Connect to one of the WebSocket endpoints: - - `/ws` (SockJS fallback supported) - - `/websocket` (native WebSocket) - - `/stomp` (native WebSocket, alternate path) -- Application destination prefix: `/app` -- Broker destinations: - - Broadcast: `/topic/...` - - User specific: `/user/queue/...` - -Examples: -- Send a generic message: send to `/app/message`, receive on `/topic/messages` -- Job updates: send to `/app/job/status`, receive on `/topic/job-updates` -- Device locations: send to `/app/device/location`, receive on `/topic/device-locations` -- Task updates (broadcast): `/topic/task-updates` -- Task specific topic: `/topic/tasks/{taskId}` - -## 2) Photo Task – New HTTP Upload Flow - -In the future, photos for a PHOTO task must NOT be embedded in the STOMP message anymore. Instead, upload the photos via HTTP POST and only send the task-completion event via STOMP without large payloads. - -### Endpoint - -- URL: `POST /api/tasks/{taskId}/photos` -- Purpose: Upload one or many photos for a task of type `PHOTO`. -- Path params: - - `taskId` (required): The MongoDB ObjectId of the task. -- Auth: (same as your app uses today; if you have a session/cookie or token, include it accordingly) -- CORS: Enabled for `*` by default on this controller. - -### Content Types (choose one) - -1) Multipart form data (recommended for binary images) - -- `Content-Type: multipart/form-data` -- Fields: - - `files`: one or multiple image files (repeat the field for multiple images) - - `completedBy` (optional): username/id of the completing user - - `note` (optional): any note (currently stored on the task when you mark it as completed via STOMP) - -Example (curl): - -```bash -curl -X POST "http://localhost:8080/api/tasks/66f5c2f1a3b6e27a1a234567/photos" \ - -H "Content-Type: multipart/form-data" \ - -F "files=@/path/to/photo1.jpg" \ - -F "files=@/path/to/photo2.jpg" \ - -F "completedBy=driver01" \ - -F "note=Anlieferung dokumentiert" -``` - -2) JSON with base64 strings (kept for compatibility) - -- `Content-Type: application/json` -- Body: - -```json -{ - "completedBy": "driver01", - "note": "Anlieferung dokumentiert", - "photos": [ - "", - "" - ] -} -``` - -Example (curl): - -```bash -curl -X POST "http://localhost:8080/api/tasks/66f5c2f1a3b6e27a1a234567/photos" \ - -H "Content-Type: application/json" \ - -d '{ - "completedBy":"driver01", - "note":"Anlieferung dokumentiert", - "photos":["iVBORw0KGgo...","iVBORw0KGgo..."] - }' -``` - -### Simple JSON endpoint (single photo per call) - -- URL: `POST /api/photos` -- Content-Type: `application/json` -- Body: - -```json -{ - "taskId": "66f5c2f1a3b6e27a1a234567", - "photo": "", - "completedBy": "driver01", - "note": "optional" -} -``` - -- Multiple photos: call this endpoint multiple times (one image per request). - -Example (curl): - -```bash -curl -X POST "http://localhost:8080/api/photos" \ - -H "Content-Type: application/json" \ - -d '{ - "taskId":"66f5c2f1a3b6e27a1a234567", - "photo":"iVBORw0KGgo...", - "completedBy":"driver01", - "note":"Anlieferung dokumentiert" - }' -``` - -### Response - -Both content types return the same JSON structure on success: - -```json -{ - "timestamp": "2025-09-13T21:27:00", - "type": "photoUploadAck", - "success": true, - "photoId": "66f5c400b1a1b05b1c123abc", - "photosCount": 2, - "taskId": "66f5c2f1a3b6e27a1a234567", - "jobId": "66f5c2f1a3b6e27a1a200000" -} -``` - -Error responses use HTTP status codes with: - -```json -{ "timestamp": "...", "type": "photoUploadAck", "success": false, "message": "..." } -``` - -### Server-side side-effect (notification) - -After a successful upload, a small event is broadcast to the specific task topic: - -- Destination: `/topic/tasks/{taskId}` -- Payload: - -```json -{ "event": "photoUploaded", "taskId": "...", "jobId": "...", "photosCount": 2, "timestamp": "..." } -``` - -Use this to update UI (e.g., show that photos have arrived) without transferring large images via STOMP. - -### Limits - -- Multipart limits (configurable in `application.properties`): - - `spring.servlet.multipart.max-file-size=32MB` - - `spring.servlet.multipart.max-request-size=64MB` -- Tomcat limits adjusted accordingly. - -If you need higher limits, we can raise these values. - -## 3) Marking Task as Completed (STOMP) - -Continue to mark a task as completed via STOMP without embedding images: - -- Send to: `/app/task/photo/completed` -- Payload (no photos inside): - -```json -{ "taskId": "66f5c2f1a3b6e27a1a234567", "completedBy": "driver01", "note": "Anlieferung dokumentiert" } -``` - -- Broadcasts an update to `/topic/task-updates` and `/topic/tasks/{taskId}`. - -## 4) Retrieval of Photos (optional) - -Currently, the backend stores uploaded photos as base64 in the `photos` collection referencing the `taskId` and `jobId`. If you need a download/list endpoint, let us know—we can expose `/api/tasks/{taskId}/photos` (GET) to return metadata and/or images. - ---- -If anything is unclear or you need different formats (e.g., presigned URLs, chunked uploads), please reach out. \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/controller/MessageController.java b/src/main/java/de/assecutor/votianlt/controller/MessageController.java index 823b3ca..7905ccf 100644 --- a/src/main/java/de/assecutor/votianlt/controller/MessageController.java +++ b/src/main/java/de/assecutor/votianlt/controller/MessageController.java @@ -27,8 +27,8 @@ import java.util.List; import java.util.Map; /** - * STOMP message controller for handling real-time communication with apps. - * Provides endpoints for sending and receiving messages via WebSocket/STOMP. + * MQTT message controller for handling real-time communication with apps. + * Provides endpoints for sending and receiving messages via WebSocket/MQTT. */ @Component @Slf4j @@ -59,14 +59,14 @@ public class MessageController { * Handles messages sent to /app/message and broadcasts them to all subscribers of /topic/messages */ public Map handleMessage(Map message) { - log.error("=== ANY MESSAGE RECEIVED === STOMP Endpoint '/app/message' called"); - log.info("STOMP Endpoint '/app/message' called with data: {}", message); + log.error("=== ANY MESSAGE RECEIVED === MQTT Endpoint '/app/message' called"); + log.info("MQTT Endpoint '/app/message' called with data: {}", message); // Add timestamp to the message message.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); message.put("processed", true); - log.info("STOMP Response for '/app/message' sent to '/topic/messages': {}", message); + log.info("MQTT Response for '/app/message' sent to '/topic/messages': {}", message); return message; } @@ -74,12 +74,12 @@ public class MessageController { * Handles job status updates from apps */ public Map handleJobStatusUpdate(Map jobUpdate) { - log.info("STOMP Endpoint '/app/job/status' called with data: {}", jobUpdate); + log.info("MQTT Endpoint '/app/job/status' called with data: {}", jobUpdate); jobUpdate.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); jobUpdate.put("source", "app"); - log.info("STOMP Response for '/app/job/status' sent to '/topic/job-updates': {}", jobUpdate); + log.info("MQTT Response for '/app/job/status' sent to '/topic/job-updates': {}", jobUpdate); return jobUpdate; } @@ -87,12 +87,12 @@ public class MessageController { * Handles device location updates from mobile apps */ public Map handleDeviceLocation(Map locationUpdate) { - log.info("STOMP Endpoint '/app/device/location' called with data: {}", locationUpdate); + log.info("MQTT Endpoint '/app/device/location' called with data: {}", locationUpdate); locationUpdate.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); locationUpdate.put("processed", true); - log.info("STOMP Response for '/app/device/location' sent to '/topic/device-locations': {}", locationUpdate); + log.info("MQTT Response for '/app/device/location' sent to '/topic/device-locations': {}", locationUpdate); return locationUpdate; } @@ -127,18 +127,18 @@ public class MessageController { } /** - * Authentication endpoint for mobile app users via STOMP. + * Authentication endpoint for mobile app users via MQTT. * Client sends to /app/auth/login with payload { email, password }. * The response is sent back to the requesting user on /user/queue/auth */ public AppLoginResponse handleAppLogin(AppLoginRequest request) { - log.info("STOMP Endpoint '/app/auth/login' called with email: {}", + log.info("MQTT Endpoint '/app/auth/login' called with email: {}", request != null ? request.getEmail() : "null"); if (request == null || request.getEmail() == null || request.getPassword() == null || request.getEmail().isBlank() || request.getPassword().isBlank()) { AppLoginResponse response = new AppLoginResponse(false, "E-Mail und Passwort sind erforderlich", null, null, null); - log.info("STOMP Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}'", + log.info("MQTT Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}'", false, "E-Mail und Passwort sind erforderlich"); return response; } @@ -146,7 +146,7 @@ public class MessageController { AppUser user = appUserRepository.findByEmail(request.getEmail()); if (user == null) { AppLoginResponse response = new AppLoginResponse(false, "Benutzer nicht gefunden", null, null, null); - log.info("STOMP Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}'", + log.info("MQTT Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}'", false, "Benutzer nicht gefunden"); return response; } @@ -154,13 +154,13 @@ public class MessageController { boolean ok = appUserService.verifyPassword(request.getPassword(), user.getPassword()); if (!ok) { AppLoginResponse response = new AppLoginResponse(false, "Ungültige Anmeldedaten", null, null, null); - log.info("STOMP Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}'", + log.info("MQTT Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}'", false, "Ungültige Anmeldedaten"); return response; } AppLoginResponse response = new AppLoginResponse(true, "Anmeldung erfolgreich", null, null, user.getIdAsString()); - log.info("STOMP Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}', appUserId='{}'", + log.info("MQTT Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}', appUserId='{}'", true, "Anmeldung erfolgreich", response.getAppUserId()); return response; } @@ -171,17 +171,17 @@ public class MessageController { * The response is sent back to the requesting user on /user/queue/jobs */ public List handleGetAssignedJobs(Map request) { - log.info("STOMP Endpoint '/app/jobs/assigned' called with data: {}", request); - log.debug("Starting to process jobs request for STOMP endpoint"); + log.info("MQTT Endpoint '/app/jobs/assigned' called with data: {}", request); + log.debug("Starting to process jobs request for MQTT endpoint"); if (request == null || !request.containsKey("appUserId")) { - log.info("STOMP Response for '/app/jobs/assigned' sent to '/user/queue/jobs': empty list (no appUserId provided)"); + log.info("MQTT Response for '/app/jobs/assigned' sent to '/user/queue/jobs': empty list (no appUserId provided)"); return List.of(); // Return empty list if no appUserId provided } String appUserId = request.get("appUserId").toString(); if (appUserId == null || appUserId.isBlank()) { - log.info("STOMP Response for '/app/jobs/assigned' sent to '/user/queue/jobs': empty list (appUserId is blank)"); + log.info("MQTT Response for '/app/jobs/assigned' sent to '/user/queue/jobs': empty list (appUserId is blank)"); return List.of(); // Return empty list if appUserId is blank } @@ -205,7 +205,7 @@ public class MessageController { }) .toList(); - log.info("STOMP Response for '/app/jobs/assigned' sent to '/user/queue/jobs': {} jobs with related data found for appUserId='{}'", + log.info("MQTT Response for '/app/jobs/assigned' sent to '/user/queue/jobs': {} jobs with related data found for appUserId='{}'", jobsWithRelatedData.size(), appUserId); // Log complete JSON for debugging @@ -214,7 +214,7 @@ public class MessageController { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.registerModule(new JavaTimeModule()); String jsonOutput = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jobsWithRelatedData); - log.info("=== COMPLETE JSON RESPONSE FOR STOMP CLIENT ==="); + log.info("=== COMPLETE JSON RESPONSE FOR MQTT CLIENT ==="); log.info("AppUserId: {}", appUserId); log.info("Number of jobs: {}", jobsWithRelatedData.size()); log.info("JSON Data:\n{}", jsonOutput); @@ -233,7 +233,7 @@ public class MessageController { * This endpoint accepts any task type (fallback for GENERIC or unknown types). */ public Map handleTaskCompleted(Map payload) { - log.info("STOMP Endpoint '/app/task/completed' called with data: {}", payload); + log.info("MQTT Endpoint '/app/task/completed' called with data: {}", payload); return processTaskCompletion(payload, null); // null means accept any task type } @@ -243,7 +243,7 @@ public class MessageController { * Broadcasts to /topic/task-updates and /topic/tasks/{taskId}. */ public Map handleTaskConfirmation(Map payload) { - log.info("STOMP Endpoint '/app/task/confirm' called with data: {}", payload); + log.info("MQTT Endpoint '/app/task/confirm' called with data: {}", payload); return processTaskCompletion(payload, "CONFIRMATION"); } @@ -254,7 +254,7 @@ public class MessageController { * Broadcasts to /topic/task-updates and /topic/tasks/{taskId}. */ public Map handlePhotoTaskCompleted(Map payload) { - log.info("STOMP Endpoint '/app/task/photo/completed' called"); + log.info("MQTT Endpoint '/app/task/photo/completed' called"); return processPhotoTaskCompletion(payload); } @@ -264,7 +264,7 @@ public class MessageController { * Broadcasts to /topic/task-updates and /topic/tasks/{taskId}. */ public Map handleSignatureTaskCompleted(Map payload) { - log.info("STOMP Endpoint '/app/task/signature/completed' called with data: {}", payload); + log.info("MQTT Endpoint '/app/task/signature/completed' called with data: {}", payload); return processTaskCompletion(payload, "SIGNATURE"); } @@ -274,7 +274,7 @@ public class MessageController { * Broadcasts to /topic/task-updates and /topic/tasks/{taskId}. */ public Map handleBarcodeTaskCompleted(Map payload) { - log.info("STOMP Endpoint '/app/task/barcode/completed' called with data: {}", payload); + log.info("MQTT Endpoint '/app/task/barcode/completed' called with data: {}", payload); return processTaskCompletion(payload, "BARCODE"); } @@ -284,7 +284,7 @@ public class MessageController { * Broadcasts to /topic/task-updates and /topic/tasks/{taskId}. */ public Map handleTodolistTaskCompleted(Map payload) { - log.info("STOMP Endpoint '/app/task/todolist/completed' called with data: {}", payload); + log.info("MQTT Endpoint '/app/task/todolist/completed' called with data: {}", payload); return processTaskCompletion(payload, "TODOLIST"); } diff --git a/src/main/java/de/assecutor/votianlt/security/SecurityConfig.java b/src/main/java/de/assecutor/votianlt/security/SecurityConfig.java index 18d78d1..b1e2914 100644 --- a/src/main/java/de/assecutor/votianlt/security/SecurityConfig.java +++ b/src/main/java/de/assecutor/votianlt/security/SecurityConfig.java @@ -45,7 +45,7 @@ public class SecurityConfig extends VaadinWebSecurity { ).permitAll() ); - // Standard-CSRF-Konfiguration (keine speziellen WebSocket/STOMP-Ausnahmen mehr notwendig) + // Standard-CSRF-Konfiguration http.csrf(csrf -> csrf .ignoringRequestMatchers( new AntPathRequestMatcher("/h2-console/**") diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d542250..03e603d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -37,8 +37,6 @@ server.tomcat.max-swallow-size=64MB # Multipart upload limits for photo HTTP uploads spring.servlet.multipart.max-file-size=32MB spring.servlet.multipart.max-request-size=64MB -# Additional WebSocket and messaging limits for large payloads -# STOMP broker relay limits # Jackson message converter limits spring.jackson.default-property-inclusion=non_null