Flutter-App mit WebView, STOMP-Client und Konfigurationssystem

- Flutter-App (app/) für iOS, Android, macOS, Windows und Linux erstellt
- WebView-Startseite mit flutter_inappwebview (iOS/Android/macOS/Windows),
  Linux-Fallback mit url_launcher
- STOMP-over-WebSocket: Topic-basierte Echtzeit-Kommunikation zwischen
  Flutter-App und Spring Boot Core
- Core: STOMP-Broker (/ws/stomp), CallEventBroadcaster auf /topic/calls,
  StompMessageController für /app/ping und /app/broadcast
- SecurityConfig: /ws/** permitAll + CSRF-Ausnahme
- Asset-basierte Konfigurationsdatei (app_config.json) für Server-URL,
  STOMP-Reconnect, Topics und WebView-URL
- launch.json um Flutter-Debug/Profile/Release-Konfigurationen erweitert
- macOS: FLTEnableMergedPlatformUIThread deaktiviert (WKWebView-Kompatibilität),
  network.client Entitlement gesetzt
- iOS: NSAllowsLocalNetworking für lokale Entwicklung
- Android: INTERNET-Permission und usesCleartextTraffic

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 10:32:14 +02:00
parent a5ed2b3355
commit ef4fa38244
134 changed files with 5961 additions and 1 deletions

View File

@@ -45,6 +45,11 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>

View File

@@ -45,8 +45,11 @@ public class SecurityConfig extends VaadinWebSecurity {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers(new AntPathRequestMatcher("/api/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/ws/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/actuator/health")).permitAll());
http.csrf(csrf -> csrf.ignoringRequestMatchers(new AntPathRequestMatcher("/api/**")));
http.csrf(csrf -> csrf
.ignoringRequestMatchers(new AntPathRequestMatcher("/api/**"))
.ignoringRequestMatchers(new AntPathRequestMatcher("/ws/**")));
super.configure(http);
setLoginView(http, LoginView.class);
}

View File

@@ -0,0 +1,46 @@
package de.assecutor.swyx.ws;
import de.assecutor.swyx.webhook.CallEventStore;
import de.assecutor.swyx.webhook.StoredCallEvent;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class CallEventBroadcaster {
public static final String TOPIC_ALL = "/topic/calls";
public static final String TOPIC_PER_TYPE_PREFIX = "/topic/calls/";
private final CallEventStore store;
private final SimpMessagingTemplate messaging;
private CallEventStore.Registration registration;
public CallEventBroadcaster(CallEventStore store, SimpMessagingTemplate messaging) {
this.store = store;
this.messaging = messaging;
}
@PostConstruct
void subscribe() {
registration = store.register(this::publish);
}
@PreDestroy
void unsubscribe() {
if (registration != null) {
registration.remove();
}
}
private void publish(StoredCallEvent event) {
messaging.convertAndSend(TOPIC_ALL, event);
if (event.eventType() != null) {
messaging.convertAndSend(TOPIC_PER_TYPE_PREFIX + event.eventType().name().toLowerCase(), event);
}
log.debug("Broadcast STOMP call event: callId={} type={}", event.callId(), event.eventType());
}
}

View File

@@ -0,0 +1,24 @@
package de.assecutor.swyx.ws;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class StompConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws/stomp").setAllowedOriginPatterns("*");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
registry.setUserDestinationPrefix("/user");
}
}

View File

@@ -0,0 +1,34 @@
package de.assecutor.swyx.ws;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Controller
public class StompMessageController {
@MessageMapping("/ping")
@SendTo("/topic/pong")
public Map<String, Object> ping(@Payload Map<String, Object> payload) {
log.info("STOMP /app/ping: {}", payload);
Map<String, Object> response = new HashMap<>(payload);
response.put("pongAt", OffsetDateTime.now().toString());
return response;
}
@MessageMapping("/broadcast")
@SendTo("/topic/messages")
public Map<String, Object> broadcast(@Payload Map<String, Object> payload) {
log.info("STOMP /app/broadcast: {}", payload);
Map<String, Object> response = new HashMap<>(payload);
response.put("broadcastAt", OffsetDateTime.now().toString());
return response;
}
}