diff --git a/.gitignore b/.gitignore
index 21f8584..04891fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,8 @@
.settings
.project
.classpath
+application-secrets.properties
+.env
*.iml
.DS_Store
diff --git a/pom.xml b/pom.xml
index 1b5d2bb..808e181 100644
--- a/pom.xml
+++ b/pom.xml
@@ -75,6 +75,10 @@
org.springframework.boot
spring-boot-starter-security
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-client
+
org.springframework.boot
spring-boot-starter-validation
diff --git a/src/main/java/de/assecutor/emulatorstation/Application.java b/src/main/java/de/assecutor/emulatorstation/Application.java
index a925040..fe2c457 100644
--- a/src/main/java/de/assecutor/emulatorstation/Application.java
+++ b/src/main/java/de/assecutor/emulatorstation/Application.java
@@ -3,14 +3,13 @@ package de.assecutor.emulatorstation;
import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.component.page.Push;
import com.vaadin.flow.theme.Theme;
+import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
-import de.assecutor.emulatorstation.pojo.UserInfo;
-import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
-
-import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@Push
@@ -18,9 +17,6 @@ import org.springframework.scheduling.annotation.EnableScheduling;
@Theme("default")
public class Application implements AppShellConfigurator {
- // Single user configuration
- public static final String SINGLE_USERNAME = "user";
- public static final String SINGLE_PASSWORD = "user123";
public static final int MAX_ACTIVE_SESSIONS = 5;
public static final Map niederlassungen = Map.ofEntries(
@@ -32,7 +28,8 @@ public class Application implements AppShellConfigurator {
Map.entry("Dresden", new NiederlassungInfo("Dresden", "172.18.0.106", "6086", "/dresden")),
Map.entry("Hannover", new NiederlassungInfo("Hannover", "172.18.0.104", "6084", "/hannover")),
Map.entry("Stuttgart", new NiederlassungInfo("Stuttgart", "172.18.0.111", "6091", "/stuttgart")),
- Map.entry("Frankfurt am Main", new NiederlassungInfo("Frankfurt am Main", "172.18.0.105", "6085", "/frankfurt")),
+ Map.entry("Frankfurt am Main",
+ new NiederlassungInfo("Frankfurt am Main", "172.18.0.105", "6085", "/frankfurt")),
Map.entry("Geschäftführung", new NiederlassungInfo("Geschäftführung", "172.18.0.112", "6092", "/gfl")));
public static final java.util.Map sessionsById = new ConcurrentHashMap<>();
diff --git a/src/main/java/de/assecutor/emulatorstation/base/domain/ContainerShutdownScheduler.java b/src/main/java/de/assecutor/emulatorstation/base/domain/ContainerShutdownScheduler.java
index c8e4b35..348fd66 100644
--- a/src/main/java/de/assecutor/emulatorstation/base/domain/ContainerShutdownScheduler.java
+++ b/src/main/java/de/assecutor/emulatorstation/base/domain/ContainerShutdownScheduler.java
@@ -1,6 +1,7 @@
package de.assecutor.emulatorstation.base.domain;
import de.assecutor.emulatorstation.Application;
+import de.assecutor.emulatorstation.base.ui.view.security.EmulatorServerConfiguration;
import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -17,9 +18,12 @@ public class ContainerShutdownScheduler {
private static final Logger logger = LoggerFactory.getLogger(ContainerShutdownScheduler.class);
- private static final String DOCKER_HOST = "172.16.0.158"; // gleiches Fallback wie in MainView/LoginView
-
private final HttpClient http = HttpClient.newHttpClient();
+ private final EmulatorServerConfiguration emulatorServerConfiguration;
+
+ public ContainerShutdownScheduler(EmulatorServerConfiguration emulatorServerConfiguration) {
+ this.emulatorServerConfiguration = emulatorServerConfiguration;
+ }
// Täglich um 22:00 Uhr Serverzeit
@Scheduled(cron = "0 0 22 * * *")
@@ -40,8 +44,10 @@ public class ContainerShutdownScheduler {
logger.info("[Scheduler] Nächtliches Herunterfahren abgeschlossen");
// Alle Sessions beenden (Logout erzwingen)
- logger.info("[Scheduler] Beginne Session-Logout für alle aktiven Sessions: {}", Application.sessionsById.size());
- for (com.vaadin.flow.server.WrappedSession session : new java.util.ArrayList<>(Application.sessionsById.values())) {
+ logger.info("[Scheduler] Beginne Session-Logout für alle aktiven Sessions: {}",
+ Application.sessionsById.size());
+ for (com.vaadin.flow.server.WrappedSession session : new java.util.ArrayList<>(
+ Application.sessionsById.values())) {
try {
session.invalidate();
} catch (Exception e) {
@@ -52,7 +58,8 @@ public class ContainerShutdownScheduler {
Application.activeSessions.clear();
Application.activeNiederlassungen.clear();
Application.sessionsById.clear();
- logger.info("[Scheduler] Session-Logout abgeschlossen. Aktive Sessions: {} / Maps geleert.", Application.activeSessions.size());
+ logger.info("[Scheduler] Session-Logout abgeschlossen. Aktive Sessions: {} / Maps geleert.",
+ Application.activeSessions.size());
}
@@ -61,10 +68,9 @@ public class ContainerShutdownScheduler {
}
private boolean isRunning(String containerName) throws Exception {
+ String dockerHost = emulatorServerConfiguration.getServerIp();
HttpRequest req = HttpRequest.newBuilder()
- .uri(URI.create("http://" + DOCKER_HOST + ":2375/containers/" + containerName + "/json"))
- .GET()
- .build();
+ .uri(URI.create("http://" + dockerHost + ":2375/containers/" + containerName + "/json")).GET().build();
HttpResponse resp = http.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() == 200) {
String body = resp.body();
@@ -81,10 +87,10 @@ public class ContainerShutdownScheduler {
}
private void stopContainer(String containerName) throws Exception {
+ String dockerHost = emulatorServerConfiguration.getServerIp();
HttpRequest req = HttpRequest.newBuilder()
- .uri(URI.create("http://" + DOCKER_HOST + ":2375/containers/" + containerName + "/stop"))
- .POST(HttpRequest.BodyPublishers.noBody())
- .build();
+ .uri(URI.create("http://" + dockerHost + ":2375/containers/" + containerName + "/stop"))
+ .POST(HttpRequest.BodyPublishers.noBody()).build();
HttpResponse resp = http.send(req, HttpResponse.BodyHandlers.ofString());
int status = resp.statusCode();
if (status == 204 || status == 304) {
@@ -92,8 +98,8 @@ public class ContainerShutdownScheduler {
} else if (status == 404) {
logger.info("[Scheduler] Container '{}' nicht gefunden (404)", containerName);
} else {
- logger.warn("[Scheduler] Unerwarteter Status beim Stoppen von '{}' -> {} / {}", containerName, status, resp.body());
+ logger.warn("[Scheduler] Unerwarteter Status beim Stoppen von '{}' -> {} / {}", containerName, status,
+ resp.body());
}
}
}
-
diff --git a/src/main/java/de/assecutor/emulatorstation/base/domain/EmulatorContainerService.java b/src/main/java/de/assecutor/emulatorstation/base/domain/EmulatorContainerService.java
new file mode 100644
index 0000000..251baf2
--- /dev/null
+++ b/src/main/java/de/assecutor/emulatorstation/base/domain/EmulatorContainerService.java
@@ -0,0 +1,44 @@
+package de.assecutor.emulatorstation.base.domain;
+
+import de.assecutor.emulatorstation.base.ui.view.security.EmulatorServerConfiguration;
+import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+@Service
+public class EmulatorContainerService {
+
+ private static final Logger logger = LoggerFactory.getLogger(EmulatorContainerService.class);
+
+ private final EmulatorServerConfiguration emulatorServerConfiguration;
+
+ public EmulatorContainerService(EmulatorServerConfiguration emulatorServerConfiguration) {
+ this.emulatorServerConfiguration = emulatorServerConfiguration;
+ }
+
+ public boolean isContainerRunning(NiederlassungInfo info) {
+ if (info == null) {
+ return false;
+ }
+
+ String server = emulatorServerConfiguration.getServerIp();
+ String containerName = "android-container-" + info.name();
+
+ try {
+ var client = java.net.http.HttpClient.newHttpClient();
+ var request = java.net.http.HttpRequest.newBuilder()
+ .uri(java.net.URI.create("http://" + server + ":2375/containers/" + containerName + "/json")).GET()
+ .build();
+ var response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
+ if (response.statusCode() == 200) {
+ String body = response.body();
+ return body != null && body.contains("\"Running\":true");
+ }
+ } catch (Exception e) {
+ logger.warn("Fehler beim Pruefen des Container-Status fuer '{}': {}", containerName, e.toString());
+ }
+
+ return false;
+ }
+}
diff --git a/src/main/java/de/assecutor/emulatorstation/base/domain/NiederlassungResolver.java b/src/main/java/de/assecutor/emulatorstation/base/domain/NiederlassungResolver.java
new file mode 100644
index 0000000..9ffdec8
--- /dev/null
+++ b/src/main/java/de/assecutor/emulatorstation/base/domain/NiederlassungResolver.java
@@ -0,0 +1,46 @@
+package de.assecutor.emulatorstation.base.domain;
+
+import de.assecutor.emulatorstation.Application;
+import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
+import de.assecutor.emulatorstation.pojo.SimCardInfo;
+import org.springframework.stereotype.Service;
+
+import java.util.Locale;
+import java.util.Map;
+
+@Service
+public class NiederlassungResolver {
+
+ private static final String SIM_PREFIX = "EMU_";
+ private static final Map NIEDERLASSUNG_BY_SIM_CODE = Map.ofEntries(Map.entry("B", "Berlin"),
+ Map.entry("HB", "Bremen"), Map.entry("HH", "Hamburg"), Map.entry("E", "Essen"), Map.entry("L", "Leipzig"),
+ Map.entry("DD", "Dresden"), Map.entry("H", "Hannover"), Map.entry("S", "Stuttgart"),
+ Map.entry("F", "Frankfurt am Main"), Map.entry("GFL", "Geschäftführung"));
+
+ public NiederlassungInfo resolveFromSimCard(SimCardInfo simCard) {
+ return simCard == null ? null : resolveFromSecretValue(simCard.secretValue());
+ }
+
+ public NiederlassungInfo resolveFromSecretValue(String secretValue) {
+ String code = extractCode(secretValue);
+ if (code == null) {
+ return null;
+ }
+
+ String niederlassungName = NIEDERLASSUNG_BY_SIM_CODE.get(code);
+ return niederlassungName == null ? null : Application.niederlassungen.get(niederlassungName);
+ }
+
+ String extractCode(String secretValue) {
+ if (secretValue == null) {
+ return null;
+ }
+
+ String normalized = secretValue.trim().toUpperCase(Locale.ROOT);
+ if (!normalized.startsWith(SIM_PREFIX) || normalized.length() <= SIM_PREFIX.length()) {
+ return null;
+ }
+
+ return normalized.substring(SIM_PREFIX.length());
+ }
+}
diff --git a/src/main/java/de/assecutor/emulatorstation/base/domain/SimCardAssignmentService.java b/src/main/java/de/assecutor/emulatorstation/base/domain/SimCardAssignmentService.java
new file mode 100644
index 0000000..c7893f9
--- /dev/null
+++ b/src/main/java/de/assecutor/emulatorstation/base/domain/SimCardAssignmentService.java
@@ -0,0 +1,118 @@
+package de.assecutor.emulatorstation.base.domain;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import de.assecutor.emulatorstation.base.ui.view.security.EmulatorServerConfiguration;
+import de.assecutor.emulatorstation.pojo.CourierInfo;
+import de.assecutor.emulatorstation.pojo.SimCardInfo;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class SimCardAssignmentService {
+
+ private final HttpClient httpClient = HttpClient.newHttpClient();
+ private final ObjectMapper objectMapper = new ObjectMapper();
+ private final EmulatorServerConfiguration emulatorServerConfiguration;
+
+ public SimCardAssignmentService(EmulatorServerConfiguration emulatorServerConfiguration) {
+ this.emulatorServerConfiguration = emulatorServerConfiguration;
+ }
+
+ public List fetchCouriers() {
+ try {
+ HttpResponse response = sendGet("/api/couriers");
+ JsonNode root = objectMapper.readTree(response.body());
+ List couriers = new ArrayList<>();
+ addCouriers(couriers, root.path("test"), "test");
+ addCouriers(couriers, root.path("live"), "live");
+ couriers.sort(Comparator.comparing(CourierInfo::environment).thenComparing(CourierInfo::courierSid));
+ return couriers;
+ } catch (IOException e) {
+ throw new IllegalStateException("Kuriere konnten nicht geladen werden.", e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Laden der Kuriere wurde unterbrochen.", e);
+ }
+ }
+
+ public List fetchSimCards() {
+ try {
+ HttpResponse response = sendGet("/api/simcards");
+ JsonNode root = objectMapper.readTree(response.body());
+ List simCards = new ArrayList<>();
+ if (root.isArray()) {
+ for (JsonNode item : root) {
+ simCards.add(new SimCardInfo(item.path("cp_id").asLong(), item.path("cust_id").asLong(),
+ item.path("usr_id").isNull() ? null : item.path("usr_id").asLong(),
+ item.path("cp_secval").asText("")));
+ }
+ }
+ simCards.sort(Comparator.comparing(SimCardInfo::secretValue));
+ return simCards;
+ } catch (IOException e) {
+ throw new IllegalStateException("SIM-Karten konnten nicht geladen werden.", e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Laden der SIM-Karten wurde unterbrochen.", e);
+ }
+ }
+
+ public void assignSimCard(CourierInfo courier, SimCardInfo simCard) {
+ try {
+ String payload = objectMapper.writeValueAsString(
+ Map.of("userId", courier.userId(), "simCardId", simCard.simCardId(), "env", courier.environment()));
+
+ HttpRequest request = HttpRequest.newBuilder().uri(buildUri("/api/simcards/assign"))
+ .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(payload))
+ .build();
+
+ HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ ensureSuccessful(response, "SIM-Karte konnte nicht zugeordnet werden.");
+ } catch (IOException e) {
+ throw new IllegalStateException("SIM-Karte konnte nicht zugeordnet werden.", e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Zuordnung der SIM-Karte wurde unterbrochen.", e);
+ }
+ }
+
+ private void addCouriers(List couriers, JsonNode nodes, String fallbackEnvironment) {
+ if (!nodes.isArray()) {
+ return;
+ }
+
+ for (JsonNode item : nodes) {
+ couriers.add(new CourierInfo(item.path("cr_id").asLong(), item.path("cr_sid").asText(""),
+ item.path("usr_id").asLong(), item.path("usr_account").asText(""),
+ item.path("env").asText(fallbackEnvironment)));
+ }
+ }
+
+ private HttpResponse sendGet(String path) throws IOException, InterruptedException {
+ HttpRequest request = HttpRequest.newBuilder().uri(buildUri(path)).GET().build();
+ HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ ensureSuccessful(response, "API-Daten konnten nicht geladen werden.");
+ return response;
+ }
+
+ private void ensureSuccessful(HttpResponse response, String message) {
+ int status = response.statusCode();
+ if (status < 200 || status >= 300) {
+ throw new IllegalStateException(message + " Status=" + status + " Body=" + response.body());
+ }
+ }
+
+ private URI buildUri(String path) {
+ return URI.create("http://" + emulatorServerConfiguration.getServerIp() + ":8086" + path);
+ }
+}
diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/AdminView.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/AdminView.java
deleted file mode 100644
index 40c44f6..0000000
--- a/src/main/java/de/assecutor/emulatorstation/base/ui/view/AdminView.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package de.assecutor.emulatorstation.base.ui.view;
-
-import com.vaadin.flow.component.UI;
-import com.vaadin.flow.component.notification.Notification;
-import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
-import com.vaadin.flow.component.orderedlayout.VerticalLayout;
-import com.vaadin.flow.component.button.Button;
-import com.vaadin.flow.component.button.ButtonVariant;
-import com.vaadin.flow.component.html.Main;
-import com.vaadin.flow.component.textfield.TextField;
-import com.vaadin.flow.router.Menu;
-import com.vaadin.flow.router.PageTitle;
-import com.vaadin.flow.router.Route;
-import com.vaadin.flow.server.VaadinService;
-import com.vaadin.flow.theme.lumo.LumoUtility;
-import jakarta.annotation.security.PermitAll;
-import util.PreferencesKeyValueStore;
-
-@Route("admin")
-@PageTitle("Admin")
-@Menu(order = 0, icon = "vaadin:clipboard-check", title = "Admin")
-@PermitAll // When security is enabled, allow all authenticated users
-public class AdminView extends Main {
- final TextField serverPortTextView;
- final Button saveBtn;
- final Button logoutBtn;
-
- final PreferencesKeyValueStore preferences = new PreferencesKeyValueStore();
-
- public AdminView() {
- addClassName(LumoUtility.Padding.MEDIUM);
-
- var verticalLayout = new VerticalLayout();
- add(verticalLayout);
-
- serverPortTextView = new TextField();
- serverPortTextView.setPlaceholder("Server");
- serverPortTextView.setValue("172.16.0.158");
- serverPortTextView.setMinWidth("20em");
- verticalLayout.add(serverPortTextView);
-
- var serverPort = preferences.get("server");
- serverPort.ifPresent(serverPortTextView::setValue);
-
- var horizontalLayout = new HorizontalLayout();
- add(horizontalLayout);
-
- saveBtn = new Button("Save", event -> savePreferences());
- saveBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
- horizontalLayout.add(saveBtn);
-
- logoutBtn = new Button("Logout", event -> logout());
- logoutBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
- horizontalLayout.add(logoutBtn);
-
- setSizeFull();
- }
-
- private void logout() {
- VaadinService.getCurrentRequest().getWrappedSession().invalidate();
-
- // Navigiere den Benutzer zur Main-Seite
- UI.getCurrent().navigate("main");
- }
-
- private void savePreferences() {
- var preferences = new PreferencesKeyValueStore();
-
- preferences.put("server", serverPortTextView.getValue());
-
- Notification.show("Daten gespeichert!", 3000, Notification.Position.MIDDLE);
-
- }
-}
diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/LoginView.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/LoginView.java
index f0d53e7..830ffea 100644
--- a/src/main/java/de/assecutor/emulatorstation/base/ui/view/LoginView.java
+++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/LoginView.java
@@ -1,147 +1,68 @@
package de.assecutor.emulatorstation.base.ui.view;
-import com.vaadin.flow.component.html.Div;
-import com.vaadin.flow.component.html.H1;
-import com.vaadin.flow.component.html.Paragraph;
-import com.vaadin.flow.component.orderedlayout.VerticalLayout;
-import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
-import com.vaadin.flow.component.textfield.PasswordField;
-import com.vaadin.flow.component.notification.Notification;
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
+import com.vaadin.flow.component.notification.Notification;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.flow.router.AfterNavigationEvent;
+import com.vaadin.flow.router.AfterNavigationObserver;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.auth.AnonymousAllowed;
-import de.assecutor.emulatorstation.Application;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.authority.SimpleGrantedAuthority;
-import org.springframework.security.core.context.SecurityContextHolder;
-import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
-
+import com.vaadin.flow.spring.security.AuthenticationContext;
+import de.assecutor.emulatorstation.base.ui.view.security.LoginSessionService;
+import de.assecutor.emulatorstation.base.ui.view.security.SsoConfiguration;
@Route("login")
@AnonymousAllowed
-public class LoginView extends VerticalLayout {
+public class LoginView extends VerticalLayout implements AfterNavigationObserver {
- private static final Logger logger = LoggerFactory.getLogger(LoginView.class);
+ private final AuthenticationContext authenticationContext;
+ private final LoginSessionService loginSessionService;
+ private final SsoConfiguration ssoConfiguration;
+
+ public LoginView(AuthenticationContext authenticationContext, LoginSessionService loginSessionService,
+ SsoConfiguration ssoConfiguration) {
+ this.authenticationContext = authenticationContext;
+ this.loginSessionService = loginSessionService;
+ this.ssoConfiguration = ssoConfiguration;
- public LoginView() {
setSizeFull();
setAlignItems(Alignment.CENTER);
setJustifyContentMode(JustifyContentMode.CENTER);
getStyle().set("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)").set("min-height", "100vh")
.set("padding", "20px");
- // Haupt-Container für das Login-Formular
Div loginContainer = new Div();
loginContainer.getStyle().set("background-color", "#ffffff").set("border-radius", "12px")
- .set("box-shadow", "0 10px 30px rgba(0,0,0,0.2)").set("padding", "40px").set("max-width", "400px")
+ .set("box-shadow", "0 10px 30px rgba(0,0,0,0.2)").set("padding", "40px").set("max-width", "440px")
.set("width", "100%").set("text-align", "center");
- // Logo/Icon
Icon icon = new Icon(VaadinIcon.DESKTOP);
icon.setSize("64px");
icon.getStyle().set("color", "#667eea").set("margin-bottom", "20px");
- // Titel
H1 title = new H1("Emulator Station");
title.getStyle().set("margin", "0 0 8px 0").set("color", "#333333").set("font-size", "2rem").set("font-weight",
"600");
- // Untertitel
- Paragraph subtitle = new Paragraph("Melden Sie sich an, um fortzufahren");
- subtitle.getStyle().set("margin", "0 0 30px 0").set("color", "#666666").set("font-size", "0.95rem");
-
- // Form-Container
VerticalLayout formLayout = new VerticalLayout();
formLayout.setSpacing(true);
formLayout.setPadding(false);
formLayout.setWidthFull();
- // Eingabefelder - nur noch Passwort
- PasswordField passwordField = new PasswordField("Passwort");
- passwordField.setWidthFull();
- passwordField.getStyle().set("margin-bottom", "16px");
-
- Select niederlassungSelect = new Select<>();
- niederlassungSelect.setLabel("Niederlassung");
- niederlassungSelect.setItems(Application.niederlassungen.keySet().stream().sorted().toList());
- niederlassungSelect.setPlaceholder("Wählen Sie eine Niederlassung");
- niederlassungSelect.setWidthFull();
- niederlassungSelect.getStyle().set("margin-bottom", "24px");
-
- Button loginButton = new Button("Anmelden", new Icon(VaadinIcon.SIGN_IN), event -> {
- String password = passwordField.getValue();
- String niederlassung = niederlassungSelect.getValue();
-
- // Validierung der Eingabefelder
- if (password.isEmpty() || niederlassung == null) {
- Notification.show("Bitte alle Felder ausfüllen", 3000, Notification.Position.MIDDLE);
- return;
- }
-
- // Passwort prüfen
- if (!Application.SINGLE_PASSWORD.equals(password)) {
- Notification.show("Ungültiges Passwort", 3000, Notification.Position.MIDDLE);
- return;
- }
-
- // Maximale Session-Anzahl prüfen
- if (Application.activeSessions.size() >= Application.MAX_ACTIVE_SESSIONS) {
- Notification.show(
- "Maximale Anzahl von " + Application.MAX_ACTIVE_SESSIONS
- + " gleichzeitigen Anmeldungen erreicht. Bitte versuchen Sie es später erneut.",
- 5000, Notification.Position.MIDDLE);
- return;
- }
-
-
- var niederlassungInfo = Application.niederlassungen.get(niederlassung);
- if (niederlassungInfo == null) {
- Notification.show("Ausgewählte Niederlassung ist ungültig", 3000, Notification.Position.MIDDLE);
- return;
- }
-
- getUI().ifPresent(ui -> {
- if (isContainerRunning(niederlassungInfo)) {
- var dialog = new com.vaadin.flow.component.dialog.Dialog();
- dialog.setHeaderTitle("Container läuft bereits");
- var text = new Paragraph("Für '" + niederlassungInfo.name() + "' läuft bereits ein Emulator-Container. Möchten Sie ihn für diese Session übernehmen?");
- var adopt = new Button("Übernehmen", e2 -> {
- dialog.close();
- completeLogin(ui, password, niederlassungInfo, true);
- });
- adopt.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
- var cancel = new Button("Abbrechen", e2 -> dialog.close());
- var buttons = new com.vaadin.flow.component.orderedlayout.HorizontalLayout(cancel, adopt);
- buttons.setWidthFull();
- buttons.setJustifyContentMode(com.vaadin.flow.component.orderedlayout.FlexComponent.JustifyContentMode.END);
- dialog.add(text, buttons);
- dialog.open();
- } else {
- completeLogin(ui, password, niederlassungInfo, false);
- }
- });
- });
-
- // Button-Styling
+ Button loginButton = new Button("Mit Microsoft anmelden", new Icon(VaadinIcon.SIGN_IN), event -> startLogin());
loginButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE);
loginButton.setWidthFull();
loginButton.getStyle().set("margin-top", "8px")
.set("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)").set("border", "none")
.set("font-weight", "600");
- // Eingabefelder zum Form-Layout hinzufügen
- formLayout.add(passwordField, niederlassungSelect, loginButton);
-
- // Alle Komponenten zum Login-Container hinzufügen
- loginContainer.add(icon, title, subtitle, formLayout);
-
- // Login-Container zur Hauptansicht hinzufügen
+ formLayout.add(loginButton);
+ loginContainer.add(icon, title, formLayout);
add(loginContainer);
if (MainLayout.instance != null) {
@@ -149,78 +70,39 @@ public class LoginView extends VerticalLayout {
}
}
- private boolean isContainerRunning(de.assecutor.emulatorstation.pojo.NiederlassungInfo info) {
- if (info == null) {
- return false;
- }
- final String srv = "172.16.0.158"; // Fallback wie in MainView
- final String name = "android-container-" + info.name();
- try {
- var client = java.net.http.HttpClient.newHttpClient();
- var request = java.net.http.HttpRequest.newBuilder()
- .uri(java.net.URI.create("http://" + srv + ":2375/containers/" + name + "/json"))
- .GET()
- .build();
- var response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
- if (response.statusCode() == 200) {
- var body = response.body();
- return body != null && body.contains("\"Running\":true");
+ @Override
+ public void afterNavigation(AfterNavigationEvent event) {
+ getUI().ifPresent(ui -> {
+ if (!ssoConfiguration.isEnabled()) {
+ ui.navigate(SimCardConfigurationView.class);
+ return;
}
- } catch (Exception e) {
- logger.warn("Fehler beim Pruefen des Container-Status fuer '{}': {}", name, e.toString());
- }
- return false;
+
+ if (authenticationContext.isAuthenticated()) {
+ if (loginSessionService.hasApplicationSession(ui.getSession())
+ && loginSessionService.hasCompletedSimConfiguration(ui.getSession())) {
+ ui.navigate(MainView.class);
+ } else {
+ ui.navigate(SimCardConfigurationView.class);
+ }
+ return;
+ }
+
+ if (event.getLocation().getQueryParameters().getParameters().containsKey("error")) {
+ Notification.show("Die Microsoft-Anmeldung konnte nicht abgeschlossen werden.", 5000,
+ Notification.Position.MIDDLE);
+ }
+ });
}
- private void completeLogin(com.vaadin.flow.component.UI ui, String password,
- de.assecutor.emulatorstation.pojo.NiederlassungInfo niederlassungInfo,
- boolean adoptRunningContainer) {
- String niederlassung = niederlassungInfo.name();
+ private void startLogin() {
+ getUI().ifPresent(ui -> {
+ if (!ssoConfiguration.isEnabled() || authenticationContext.isAuthenticated()) {
+ ui.navigate(SimCardConfigurationView.class);
+ return;
+ }
- String sessionId = ui.getSession().getSession().getId();
- // Session niemals ablaufen lassen
- ui.getSession().getSession().setMaxInactiveInterval(-1);
-
- // HttpSession/WrappedSession im Registry merken, um später invalidieren zu können
- Application.sessionsById.put(sessionId, ui.getSession().getSession());
-
-
- // Session registrieren
- Application.activeSessions.put(sessionId, niederlassung);
- Application.activeNiederlassungen.put(niederlassung, sessionId);
-
- // Log that Niederlassung is now blocked
- logger.info("Niederlassung '{}' wurde gesperrt fuer Session {}", niederlassung, sessionId);
- logger.info("Aktive Niederlassungen: {}", Application.activeNiederlassungen.keySet());
-
- // Spring Security Authentifizierung setzen
- var authorities = java.util.List.of(new org.springframework.security.core.authority.SimpleGrantedAuthority("ROLE_USER"));
- var authentication = new org.springframework.security.authentication.UsernamePasswordAuthenticationToken(
- Application.SINGLE_USERNAME, password, authorities);
- org.springframework.security.core.context.SecurityContextHolder.getContext().setAuthentication(authentication);
-
- // Spring Security Kontext explizit in der HTTP-Session speichern, damit Reload eingeloggt bleibt
- ui.getSession().getSession().setAttribute(
- org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
- org.springframework.security.core.context.SecurityContextHolder.getContext());
-
- // Session-Daten setzen
- ui.getSession().setAttribute("user", Application.SINGLE_USERNAME);
- ui.getSession().setAttribute("username", Application.SINGLE_USERNAME);
- ui.getSession().setAttribute("niederlassung", niederlassungInfo);
- ui.getSession().setAttribute("sessionId", sessionId);
- if (adoptRunningContainer) {
- // UI soll direkt in gestarteten Zustand gehen
- ui.getSession().setAttribute("emulatorStarted", true);
- }
-
- logger.info("Login erfolgreich - Session-Daten gesetzt:");
- logger.info("Username: {}", Application.SINGLE_USERNAME);
- logger.info("Niederlassung: {}", niederlassungInfo.name());
- logger.info("SessionId: {}", sessionId);
- logger.info("Aktive Sessions: {}/{}", Application.activeSessions.size(), Application.MAX_ACTIVE_SESSIONS);
-
- ui.navigate("main");
+ ui.getPage().setLocation("/oauth2/authorization/azure");
+ });
}
-
}
diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainLayout.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainLayout.java
index a89ce8e..6fc3a36 100644
--- a/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainLayout.java
+++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainLayout.java
@@ -16,14 +16,14 @@ import com.vaadin.flow.component.sidenav.SideNav;
import com.vaadin.flow.component.sidenav.SideNavItem;
import com.vaadin.flow.router.Layout;
import com.vaadin.flow.server.VaadinSession;
+import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.flow.server.menu.MenuConfiguration;
import com.vaadin.flow.server.menu.MenuEntry;
-import jakarta.annotation.security.PermitAll;
import static com.vaadin.flow.theme.lumo.LumoUtility.*;
@Layout
-@PermitAll // When security is enabled, allow all authenticated users
+@AnonymousAllowed
public final class MainLayout extends AppLayout {
public static MainLayout instance = null;
@@ -54,7 +54,6 @@ public final class MainLayout extends AppLayout {
}
private Div createHeader() {
- // TODO Replace with real application logo and name
var appLogo = VaadinIcon.CUBES.create();
appLogo.addClassNames(TextColor.PRIMARY, IconSize.LARGE);
@@ -82,7 +81,6 @@ public final class MainLayout extends AppLayout {
}
private Component createUserMenu() {
- // TODO Replace with real user information and actions
var avatar = new Avatar("John Smith");
avatar.addThemeVariants(AvatarVariant.LUMO_XSMALL);
avatar.addClassNames(Margin.Right.SMALL);
diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainView.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainView.java
index 8f1b140..7c63842 100644
--- a/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainView.java
+++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainView.java
@@ -22,12 +22,15 @@ import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.PreserveOnRefresh;
-import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.flow.server.VaadinSession;
+import com.vaadin.flow.server.auth.AnonymousAllowed;
+import com.vaadin.flow.spring.security.AuthenticationContext;
import de.assecutor.emulatorstation.pojo.ExecResponse;
import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
import de.assecutor.emulatorstation.Application;
-import jakarta.annotation.security.PermitAll;
+import de.assecutor.emulatorstation.base.ui.view.security.EmulatorServerConfiguration;
+import de.assecutor.emulatorstation.base.ui.view.security.LoginSessionService;
+import de.assecutor.emulatorstation.base.ui.view.security.SsoConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -38,12 +41,15 @@ import java.net.http.HttpResponse;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-@PermitAll // When security is enabled, allow all authenticated users
+@AnonymousAllowed
@Route("main")
@PreserveOnRefresh
-@AnonymousAllowed
public final class MainView extends Main implements BeforeEnterObserver {
private static final Logger logger = LoggerFactory.getLogger(MainView.class);
+ private final AuthenticationContext authenticationContext;
+ private final EmulatorServerConfiguration emulatorServerConfiguration;
+ private final LoginSessionService loginSessionService;
+ private final SsoConfiguration ssoConfiguration;
private String username;
private NiederlassungInfo niederlassung;
private String server;
@@ -55,7 +61,13 @@ public final class MainView extends Main implements BeforeEnterObserver {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
- MainView() {
+ MainView(AuthenticationContext authenticationContext, EmulatorServerConfiguration emulatorServerConfiguration,
+ LoginSessionService loginSessionService, SsoConfiguration ssoConfiguration) {
+ this.authenticationContext = authenticationContext;
+ this.emulatorServerConfiguration = emulatorServerConfiguration;
+ this.loginSessionService = loginSessionService;
+ this.ssoConfiguration = ssoConfiguration;
+
setSizeFull();
getStyle().set("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)").set("min-height", "100vh")
.set("display", "flex").set("flex-direction", "column").set("align-items", "center")
@@ -66,7 +78,6 @@ public final class MainView extends Main implements BeforeEnterObserver {
contentContainer.getStyle().set("width", "95%").set("height", "95%").set("display", "flex")
.set("flex-direction", "column").set("box-sizing", "border-box");
-
setupWelcomeMessage(contentContainer);
setupEmulatorContainer(contentContainer);
setupButtonLayout(contentContainer);
@@ -148,8 +159,6 @@ public final class MainView extends Main implements BeforeEnterObserver {
}
}
-
-
private void setupWelcomeMessage(Div container) {
welcomeMessage.addClassName("welcome-panel");
welcomeMessage.getStyle().set("display", "flex").set("flex-direction", "column").set("align-items", "center")
@@ -226,7 +235,6 @@ public final class MainView extends Main implements BeforeEnterObserver {
var vaadinSession = currentUI.get().getSession();
username = (String) vaadinSession.getAttribute("username");
niederlassung = (NiederlassungInfo) vaadinSession.getAttribute("niederlassung");
- server = (String) vaadinSession.getAttribute("server");
// Fallback: Nach Reload Niederlassung anhand der aktiven Sessions rekonstruieren
if (niederlassung == null) {
@@ -254,9 +262,7 @@ public final class MainView extends Main implements BeforeEnterObserver {
logger.error("UI nicht verfügbar - kann Session-Daten nicht laden");
}
- if (server == null) {
- server = "172.16.0.158";
- }
+ server = emulatorServerConfiguration.getServerIp();
logger.info("MainView Session-Daten geladen:");
logger.info("Username: {}", username);
@@ -267,7 +273,8 @@ public final class MainView extends Main implements BeforeEnterObserver {
private void restoreUIStateIfNeeded() {
var uiOpt = getUI();
- if (uiOpt.isEmpty()) return;
+ if (uiOpt.isEmpty())
+ return;
var session = uiOpt.get().getSession();
boolean emulatorStarted = Boolean.TRUE.equals(session.getAttribute("emulatorStarted"));
@@ -286,7 +293,25 @@ public final class MainView extends Main implements BeforeEnterObserver {
@Override
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
- // Kein Redirect mehr zur Login-Seite bei fehlender Session
+ VaadinSession session = VaadinSession.getCurrent();
+
+ if (!ssoConfiguration.isEnabled()) {
+ if (!loginSessionService.hasApplicationSession(session)
+ || !loginSessionService.hasCompletedSimConfiguration(session)) {
+ beforeEnterEvent.rerouteTo(SimCardConfigurationView.class);
+ }
+ return;
+ }
+
+ if (!authenticationContext.isAuthenticated()) {
+ beforeEnterEvent.rerouteTo(LoginView.class);
+ return;
+ }
+
+ if (!loginSessionService.hasApplicationSession(session)
+ || !loginSessionService.hasCompletedSimConfiguration(session)) {
+ beforeEnterEvent.rerouteTo(SimCardConfigurationView.class);
+ }
}
private void startup() {
@@ -609,6 +634,7 @@ public final class MainView extends Main implements BeforeEnterObserver {
logger.error("Fehler beim HTTP-Request", e);
}
}
+
private String buildContainerName(NiederlassungInfo info) {
return "android-container-" + info.name();
}
@@ -618,14 +644,12 @@ public final class MainView extends Main implements BeforeEnterObserver {
logger.warn("isContainerStarted: Niederlassung ist null");
return false;
}
- String srv = (this.server != null ? this.server : "172.16.0.158");
+ String srv = this.server != null ? this.server : emulatorServerConfiguration.getServerIp();
String name = buildContainerName(info);
try {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create("http://" + srv + ":2375/containers/" + name + "/json"))
- .GET()
- .build();
+ .uri(URI.create("http://" + srv + ":2375/containers/" + name + "/json")).GET().build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
int status = response.statusCode();
logger.info("Inspect-Container '{}': status={} ", name, status);
@@ -645,7 +669,6 @@ public final class MainView extends Main implements BeforeEnterObserver {
return false;
}
-
private Dialog showWaitDialog(String message) {
Dialog dialog = new Dialog();
dialog.setModal(true);
@@ -735,45 +758,12 @@ public final class MainView extends Main implements BeforeEnterObserver {
logger.info("Starte Logout-Cleanup nach Shutdown");
dialog.close();
- // Session cleanup erst nach Shutdown
- String sessionId = (String) ui.getSession().getAttribute("sessionId");
- if (sessionId != null) {
- Application.activeSessions.remove(sessionId);
- logger.info("Session {} aus aktiven Sessions entfernt", sessionId);
- Application.sessionsById.remove(sessionId);
-
+ loginSessionService.cleanupApplicationSession(ui.getSession());
+ if (ssoConfiguration.isEnabled()) {
+ authenticationContext.logout();
+ } else {
+ ui.navigate(SimCardConfigurationView.class);
}
- if (niederlassung != null) {
- Application.activeNiederlassungen.remove(niederlassung.name());
- logger.info("Niederlassung {} aus aktiven Niederlassungen entfernt",
- niederlassung.name());
- }
- logger.info("Aktive Sessions nach Logout: {}/{}", Application.activeSessions.size(),
- Application.MAX_ACTIVE_SESSIONS);
-
- // 1) Client-seitigen Redirect sofort ausführen (wird beim Client direkt verarbeitet)
- var httpSession = ui.getSession().getSession();
- var vaadinSession = ui.getSession();
- ui.getPage().executeJs("window.location.replace($0);", "login");
-
- // 2) Session-Schließung leicht verzögert im Hintergrund, damit der Redirect sicher ankommt
- executor.submit(() -> {
- try {
- Thread.sleep(300); // kurze Verzögerung reicht i.d.R.
- } catch (InterruptedException ignored) {
- }
- try {
- vaadinSession.close();
- } catch (Exception e) {
- logger.warn("Fehler beim Schließen der VaadinSession nach Redirect", e);
- }
- try {
- httpSession.invalidate();
- } catch (Exception e) {
- logger.warn("Fehler beim Invalidieren der HttpSession nach Redirect", e);
- }
- logger.info("Session nach Redirect geschlossen/invalidiert");
- });
} catch (Exception ex) {
logger.error("Fehler beim Logout UI-Update", ex);
diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/RootView.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/RootView.java
index 6cc29ab..6a62eba 100644
--- a/src/main/java/de/assecutor/emulatorstation/base/ui/view/RootView.java
+++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/RootView.java
@@ -3,12 +3,21 @@ package de.assecutor.emulatorstation.base.ui.view;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.Route;
+import com.vaadin.flow.server.auth.AnonymousAllowed;
+import de.assecutor.emulatorstation.base.ui.view.security.SsoConfiguration;
@Route("")
+@AnonymousAllowed
public class RootView implements BeforeEnterObserver {
+ private final SsoConfiguration ssoConfiguration;
+
+ public RootView(SsoConfiguration ssoConfiguration) {
+ this.ssoConfiguration = ssoConfiguration;
+ }
+
@Override
public void beforeEnter(BeforeEnterEvent event) {
- event.rerouteTo("main");
+ event.rerouteTo(ssoConfiguration.isEnabled() ? "login" : "sim-config");
}
}
diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/SimCardConfigurationView.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/SimCardConfigurationView.java
new file mode 100644
index 0000000..9283076
--- /dev/null
+++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/SimCardConfigurationView.java
@@ -0,0 +1,236 @@
+package de.assecutor.emulatorstation.base.ui.view;
+
+import com.vaadin.flow.component.button.Button;
+import com.vaadin.flow.component.button.ButtonVariant;
+import com.vaadin.flow.component.combobox.ComboBox;
+import com.vaadin.flow.component.dialog.Dialog;
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.H1;
+import com.vaadin.flow.component.html.Paragraph;
+import com.vaadin.flow.component.icon.Icon;
+import com.vaadin.flow.component.icon.VaadinIcon;
+import com.vaadin.flow.component.notification.Notification;
+import com.vaadin.flow.component.orderedlayout.FlexComponent;
+import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.flow.router.BeforeEnterEvent;
+import com.vaadin.flow.router.BeforeEnterObserver;
+import com.vaadin.flow.router.PageTitle;
+import com.vaadin.flow.router.Route;
+import com.vaadin.flow.server.VaadinSession;
+import com.vaadin.flow.server.auth.AnonymousAllowed;
+import com.vaadin.flow.spring.security.AuthenticationContext;
+import de.assecutor.emulatorstation.base.domain.EmulatorContainerService;
+import de.assecutor.emulatorstation.base.domain.NiederlassungResolver;
+import de.assecutor.emulatorstation.base.domain.SimCardAssignmentService;
+import de.assecutor.emulatorstation.base.ui.view.security.LoginSessionService;
+import de.assecutor.emulatorstation.base.ui.view.security.SsoConfiguration;
+import de.assecutor.emulatorstation.pojo.CourierInfo;
+import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
+import de.assecutor.emulatorstation.pojo.SimCardInfo;
+
+import java.util.List;
+
+@Route("sim-config")
+@PageTitle("SIM-Konfiguration")
+@AnonymousAllowed
+public class SimCardConfigurationView extends VerticalLayout implements BeforeEnterObserver {
+
+ private final AuthenticationContext authenticationContext;
+ private final LoginSessionService loginSessionService;
+ private final EmulatorContainerService emulatorContainerService;
+ private final NiederlassungResolver niederlassungResolver;
+ private final SimCardAssignmentService simCardAssignmentService;
+ private final SsoConfiguration ssoConfiguration;
+
+ private final ComboBox courierSelect = new ComboBox<>("Kurier");
+ private final ComboBox simCardSelect = new ComboBox<>("SIM-Karte");
+ private final Paragraph statusText = new Paragraph("Lade Kuriere und SIM-Karten...");
+ private final Button assignButton;
+
+ public SimCardConfigurationView(AuthenticationContext authenticationContext,
+ LoginSessionService loginSessionService, EmulatorContainerService emulatorContainerService,
+ NiederlassungResolver niederlassungResolver, SimCardAssignmentService simCardAssignmentService,
+ SsoConfiguration ssoConfiguration) {
+ this.authenticationContext = authenticationContext;
+ this.loginSessionService = loginSessionService;
+ this.emulatorContainerService = emulatorContainerService;
+ this.niederlassungResolver = niederlassungResolver;
+ this.simCardAssignmentService = simCardAssignmentService;
+ this.ssoConfiguration = ssoConfiguration;
+
+ setSizeFull();
+ setAlignItems(Alignment.CENTER);
+ setJustifyContentMode(JustifyContentMode.CENTER);
+ getStyle().set("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)").set("min-height", "100vh")
+ .set("padding", "20px");
+
+ Div container = new Div();
+ container.getStyle().set("background-color", "#ffffff").set("border-radius", "12px")
+ .set("box-shadow", "0 10px 30px rgba(0,0,0,0.2)").set("padding", "40px").set("max-width", "560px")
+ .set("width", "100%");
+
+ Icon icon = new Icon(VaadinIcon.MOBILE);
+ icon.setSize("56px");
+ icon.getStyle().set("color", "#667eea").set("margin-bottom", "18px");
+
+ H1 title = new H1("SIM-Karte zuordnen");
+ title.getStyle().set("margin", "0 0 8px 0").set("color", "#333333").set("font-size", "2rem").set("font-weight",
+ "600");
+
+ Paragraph subtitle = new Paragraph(
+ "Wählen Sie einen Kurier und eine SIM-Karte. Danach gelangen Sie zur Hauptseite.");
+ subtitle.getStyle().set("margin", "0 0 8px 0").set("color", "#666666").set("font-size", "0.95rem");
+
+ courierSelect.setWidthFull();
+ courierSelect.setPlaceholder("Kurier auswählen");
+ courierSelect.setItemLabelGenerator(CourierInfo::displayLabel);
+
+ simCardSelect.setWidthFull();
+ simCardSelect.setPlaceholder("SIM-Karte auswählen");
+ simCardSelect.setItemLabelGenerator(SimCardInfo::displayLabel);
+
+ assignButton = new Button("Weiter", new Icon(VaadinIcon.CHECK), event -> assignSimCard());
+ assignButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
+ assignButton.setEnabled(false);
+
+ HorizontalLayout actions = new HorizontalLayout(assignButton);
+ actions.setSpacing(true);
+
+ statusText.getStyle().set("margin", "18px 0 0 0").set("color", "#666666").set("font-size", "0.9rem");
+
+ VerticalLayout form = new VerticalLayout(icon, title, subtitle, courierSelect, simCardSelect, actions,
+ statusText);
+ form.setSpacing(true);
+ form.setPadding(false);
+ form.setWidthFull();
+
+ container.add(form);
+ add(container);
+
+ addAttachListener(event -> loadOptions());
+
+ if (MainLayout.instance != null) {
+ MainLayout.instance.setDrawerOpened(false);
+ }
+ }
+
+ @Override
+ public void beforeEnter(BeforeEnterEvent event) {
+ VaadinSession session = VaadinSession.getCurrent();
+
+ if (ssoConfiguration.isEnabled() && !authenticationContext.isAuthenticated()) {
+ event.rerouteTo(LoginView.class);
+ return;
+ }
+
+ if (loginSessionService.hasApplicationSession(session)
+ && loginSessionService.hasCompletedSimConfiguration(session)) {
+ event.rerouteTo(MainView.class);
+ }
+ }
+
+ private void loadOptions() {
+ try {
+ List couriers = simCardAssignmentService.fetchCouriers();
+ List simCards = simCardAssignmentService.fetchSimCards();
+
+ courierSelect.setItems(couriers);
+ simCardSelect.setItems(simCards);
+ courierSelect.clear();
+ simCardSelect.clear();
+
+ boolean hasData = !couriers.isEmpty() && !simCards.isEmpty();
+ assignButton.setEnabled(hasData);
+
+ if (hasData) {
+ statusText.setText("Es stehen " + couriers.size() + " Kuriere und " + simCards.size()
+ + " SIM-Karten zur Auswahl.");
+ } else {
+ statusText.setText("Es wurden keine Kuriere oder SIM-Karten gefunden.");
+ }
+ } catch (Exception ex) {
+ assignButton.setEnabled(false);
+ showError(ex.getMessage());
+ }
+ }
+
+ private void assignSimCard() {
+ CourierInfo courier = courierSelect.getValue();
+ SimCardInfo simCard = simCardSelect.getValue();
+
+ if (courier == null || simCard == null) {
+ Notification.show("Bitte wählen Sie einen Kurier und eine SIM-Karte aus.", 3000,
+ Notification.Position.MIDDLE);
+ return;
+ }
+
+ NiederlassungInfo niederlassungInfo = niederlassungResolver.resolveFromSimCard(simCard);
+ if (niederlassungInfo == null) {
+ showError("SIM-Karte '" + simCard.secretValue() + "' definiert keine gültige Niederlassung.");
+ return;
+ }
+
+ getUI().ifPresent(ui -> {
+ if (emulatorContainerService.isContainerRunning(niederlassungInfo)) {
+ confirmContainerAdoption(ui, courier, simCard, niederlassungInfo);
+ } else {
+ continueAssignment(ui, courier, simCard, niederlassungInfo, false);
+ }
+ });
+ }
+
+ private void confirmContainerAdoption(com.vaadin.flow.component.UI ui, CourierInfo courier, SimCardInfo simCard,
+ NiederlassungInfo niederlassungInfo) {
+ Dialog dialog = new Dialog();
+ dialog.setHeaderTitle("Container läuft bereits");
+
+ Paragraph text = new Paragraph("Für '" + niederlassungInfo.name()
+ + "' läuft bereits ein Emulator-Container. Möchten Sie ihn für diese Session übernehmen?");
+
+ Button adoptButton = new Button("Übernehmen", event -> {
+ dialog.close();
+ continueAssignment(ui, courier, simCard, niederlassungInfo, true);
+ });
+ adoptButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
+
+ Button cancelButton = new Button("Abbrechen", event -> dialog.close());
+
+ HorizontalLayout buttons = new HorizontalLayout(cancelButton, adoptButton);
+ buttons.setWidthFull();
+ buttons.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
+
+ dialog.add(text, buttons);
+ dialog.open();
+ }
+
+ private void continueAssignment(com.vaadin.flow.component.UI ui, CourierInfo courier, SimCardInfo simCard,
+ NiederlassungInfo niederlassungInfo, boolean adoptRunningContainer) {
+ assignButton.setEnabled(false);
+ statusText.setText("Zuordnung wird gespeichert...");
+
+ String error = loginSessionService.initializeApplicationSession(ui, niederlassungInfo, adoptRunningContainer);
+ if (error != null) {
+ assignButton.setEnabled(true);
+ showError(error);
+ return;
+ }
+
+ try {
+ simCardAssignmentService.assignSimCard(courier, simCard);
+ loginSessionService.markSimConfigurationCompleted(ui.getSession());
+ statusText.setText("SIM-Karte wurde erfolgreich zugeordnet.");
+ Notification.show("SIM-Karte erfolgreich zugeordnet.", 3000, Notification.Position.MIDDLE);
+ ui.navigate(MainView.class);
+ } catch (Exception ex) {
+ loginSessionService.cleanupApplicationSession(ui.getSession());
+ assignButton.setEnabled(true);
+ showError(ex.getMessage());
+ }
+ }
+
+ private void showError(String message) {
+ statusText.setText(message);
+ Notification.show(message, 5000, Notification.Position.MIDDLE);
+ }
+}
diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/EmulatorServerConfiguration.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/EmulatorServerConfiguration.java
new file mode 100644
index 0000000..5ecd60f
--- /dev/null
+++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/EmulatorServerConfiguration.java
@@ -0,0 +1,18 @@
+package de.assecutor.emulatorstation.base.ui.view.security;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Service
+public class EmulatorServerConfiguration {
+
+ private final String serverIp;
+
+ public EmulatorServerConfiguration(@Value("${app.emulator.server-ip:172.16.0.158}") String serverIp) {
+ this.serverIp = serverIp;
+ }
+
+ public String getServerIp() {
+ return serverIp;
+ }
+}
diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/LoginSessionService.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/LoginSessionService.java
new file mode 100644
index 0000000..7d28cb5
--- /dev/null
+++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/LoginSessionService.java
@@ -0,0 +1,183 @@
+package de.assecutor.emulatorstation.base.ui.view.security;
+
+import com.vaadin.flow.component.UI;
+import com.vaadin.flow.server.VaadinSession;
+import com.vaadin.flow.server.WrappedSession;
+import de.assecutor.emulatorstation.Application;
+import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.stereotype.Service;
+
+@Service
+public class LoginSessionService {
+
+ private static final Logger logger = LoggerFactory.getLogger(LoginSessionService.class);
+
+ private static final String USER_ATTRIBUTE = "user";
+ private static final String USERNAME_ATTRIBUTE = "username";
+ private static final String NIEDERLASSUNG_ATTRIBUTE = "niederlassung";
+ private static final String SESSION_ID_ATTRIBUTE = "sessionId";
+ private static final String EMULATOR_STARTED_ATTRIBUTE = "emulatorStarted";
+ private static final String SIM_CONFIGURATION_COMPLETED_ATTRIBUTE = "simConfigurationCompleted";
+
+ public boolean hasApplicationSession(VaadinSession session) {
+ if (session == null) {
+ return false;
+ }
+ if (session.getAttribute(NIEDERLASSUNG_ATTRIBUTE) instanceof NiederlassungInfo) {
+ return true;
+ }
+
+ WrappedSession wrappedSession = getWrappedSession(session);
+ return wrappedSession != null && Application.activeSessions.containsKey(wrappedSession.getId());
+ }
+
+ public boolean hasCompletedSimConfiguration(VaadinSession session) {
+ return session != null && Boolean.TRUE.equals(session.getAttribute(SIM_CONFIGURATION_COMPLETED_ATTRIBUTE));
+ }
+
+ public void markSimConfigurationCompleted(VaadinSession session) {
+ if (session == null) {
+ return;
+ }
+ session.setAttribute(SIM_CONFIGURATION_COMPLETED_ATTRIBUTE, true);
+ }
+
+ public String initializeApplicationSession(UI ui, NiederlassungInfo niederlassungInfo,
+ boolean adoptRunningContainer) {
+ if (ui == null) {
+ return "UI ist nicht verfügbar.";
+ }
+
+ VaadinSession vaadinSession = ui.getSession();
+ WrappedSession wrappedSession = getWrappedSession(vaadinSession);
+ if (wrappedSession == null) {
+ return "Keine HTTP-Session verfügbar.";
+ }
+ if (niederlassungInfo == null) {
+ return "Die ermittelte Niederlassung ist ungültig.";
+ }
+
+ String sessionId = wrappedSession.getId();
+ if (!Application.activeSessions.containsKey(sessionId)
+ && Application.activeSessions.size() >= Application.MAX_ACTIVE_SESSIONS) {
+ return "Maximale Anzahl von " + Application.MAX_ACTIVE_SESSIONS
+ + " gleichzeitigen Anmeldungen erreicht. Bitte versuchen Sie es später erneut.";
+ }
+
+ String username = resolveCurrentUsername();
+
+ wrappedSession.setMaxInactiveInterval(-1);
+ Application.sessionsById.put(sessionId, wrappedSession);
+ Application.activeSessions.put(sessionId, niederlassungInfo.name());
+ Application.activeNiederlassungen.put(niederlassungInfo.name(), sessionId);
+
+ vaadinSession.setAttribute(USER_ATTRIBUTE, username);
+ vaadinSession.setAttribute(USERNAME_ATTRIBUTE, username);
+ vaadinSession.setAttribute(NIEDERLASSUNG_ATTRIBUTE, niederlassungInfo);
+ vaadinSession.setAttribute(SESSION_ID_ATTRIBUTE, sessionId);
+ if (adoptRunningContainer) {
+ vaadinSession.setAttribute(EMULATOR_STARTED_ATTRIBUTE, true);
+ } else {
+ vaadinSession.setAttribute(EMULATOR_STARTED_ATTRIBUTE, null);
+ }
+ vaadinSession.setAttribute(SIM_CONFIGURATION_COMPLETED_ATTRIBUTE, null);
+
+ logger.info("Login erfolgreich - Session-Daten gesetzt:");
+ logger.info("Username: {}", username);
+ logger.info("Niederlassung: {}", niederlassungInfo.name());
+ logger.info("SessionId: {}", sessionId);
+ logger.info("Aktive Sessions: {}/{}", Application.activeSessions.size(), Application.MAX_ACTIVE_SESSIONS);
+
+ return null;
+ }
+
+ public void cleanupApplicationSession(VaadinSession session) {
+ if (session == null) {
+ return;
+ }
+
+ WrappedSession wrappedSession = getWrappedSession(session);
+ String sessionId = wrappedSession != null ? wrappedSession.getId() : null;
+ NiederlassungInfo niederlassungInfo = (NiederlassungInfo) session.getAttribute(NIEDERLASSUNG_ATTRIBUTE);
+
+ cleanupApplicationSession(sessionId, niederlassungInfo != null ? niederlassungInfo.name() : null);
+
+ session.setAttribute(USER_ATTRIBUTE, null);
+ session.setAttribute(USERNAME_ATTRIBUTE, null);
+ session.setAttribute(NIEDERLASSUNG_ATTRIBUTE, null);
+ session.setAttribute(SESSION_ID_ATTRIBUTE, null);
+ session.setAttribute(EMULATOR_STARTED_ATTRIBUTE, null);
+ session.setAttribute(SIM_CONFIGURATION_COMPLETED_ATTRIBUTE, null);
+ }
+
+ public void cleanupApplicationSession(String sessionId) {
+ cleanupApplicationSession(sessionId, null);
+ }
+
+ private void cleanupApplicationSession(String sessionId, String niederlassungName) {
+ if (sessionId == null || sessionId.isBlank()) {
+ return;
+ }
+
+ String activeNiederlassung = niederlassungName != null
+ ? niederlassungName
+ : Application.activeSessions.get(sessionId);
+ Application.activeSessions.remove(sessionId);
+ Application.sessionsById.remove(sessionId);
+
+ if (activeNiederlassung != null) {
+ Application.activeNiederlassungen.remove(activeNiederlassung);
+ logger.info("Niederlassung {} aus aktiven Niederlassungen entfernt", activeNiederlassung);
+ }
+
+ logger.info("Session {} aus aktiven Sessions entfernt", sessionId);
+ logger.info("Aktive Sessions nach Cleanup: {}/{}", Application.activeSessions.size(),
+ Application.MAX_ACTIVE_SESSIONS);
+ }
+
+ private WrappedSession getWrappedSession(VaadinSession session) {
+ return session != null ? session.getSession() : null;
+ }
+
+ private boolean isAuthenticated(Authentication authentication) {
+ return authentication != null && authentication.isAuthenticated()
+ && !(authentication instanceof AnonymousAuthenticationToken);
+ }
+
+ private String resolveCurrentUsername() {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (isAuthenticated(authentication)) {
+ return resolveUsername(authentication);
+ }
+
+ return "lokal";
+ }
+
+ private String resolveUsername(Authentication authentication) {
+ Object principal = authentication.getPrincipal();
+ if (principal instanceof OidcUser oidcUser) {
+ String preferredUsername = oidcUser.getPreferredUsername();
+ if (preferredUsername != null && !preferredUsername.isBlank()) {
+ return preferredUsername;
+ }
+
+ String email = oidcUser.getEmail();
+ if (email != null && !email.isBlank()) {
+ return email;
+ }
+
+ String fullName = oidcUser.getFullName();
+ if (fullName != null && !fullName.isBlank()) {
+ return fullName;
+ }
+ }
+
+ return authentication.getName();
+ }
+}
diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/SecurityConfig.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/SecurityConfig.java
index ff68f96..db7efc5 100644
--- a/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/SecurityConfig.java
+++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/SecurityConfig.java
@@ -4,32 +4,50 @@ import com.vaadin.flow.spring.security.VaadinWebSecurity;
import de.assecutor.emulatorstation.base.ui.view.LoginView;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import de.assecutor.emulatorstation.Application;
@Configuration
public class SecurityConfig extends VaadinWebSecurity {
+ private final LoginSessionService loginSessionService;
+
+ public SecurityConfig(LoginSessionService loginSessionService) {
+ this.loginSessionService = loginSessionService;
+ }
+
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
setLoginView(http, LoginView.class);
+ LogoutSuccessHandler logoutSuccessHandler = oidcLogoutSuccessHandler("{baseUrl}/login");
+
http.sessionManagement(session -> session
// Wichtig: Session-Fixation so konfigurieren, dass Session-Attribute (z.B. "user") erhalten bleiben
- .sessionFixation(fixation -> fixation.migrateSession())
- .maximumSessions(Application.MAX_ACTIVE_SESSIONS)
+ .sessionFixation(fixation -> fixation.migrateSession()).maximumSessions(Application.MAX_ACTIVE_SESSIONS)
.maxSessionsPreventsLogin(false))
- .logout(
- logout -> logout.logoutSuccessUrl("/login").addLogoutHandler((request, response, authentication) -> {
- if (authentication != null) {
- String username = authentication.getName();
- Application.activeNiederlassungen.entrySet()
- .removeIf(entry -> entry.getValue().equals(username));
+ .oauth2Login(oauth2 -> oauth2.loginPage("/login").defaultSuccessUrl("/login", true)
+ .failureUrl("/login?error"))
+ .logout(logout -> {
+ logout.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET")).invalidateHttpSession(true)
+ .clearAuthentication(true).deleteCookies("JSESSIONID")
+ .addLogoutHandler((request, response, authentication) -> {
+ var session = request.getSession(false);
+ if (session != null) {
+ loginSessionService.cleanupApplicationSession(session.getId());
+ }
+ });
+
+ if (logoutSuccessHandler != null) {
+ logout.logoutSuccessHandler(logoutSuccessHandler);
+ } else {
+ logout.logoutSuccessUrl("/login");
}
- }));
+ });
}
}
diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/SessionListener.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/SessionListener.java
index d997590..d271dc6 100644
--- a/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/SessionListener.java
+++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/SessionListener.java
@@ -11,11 +11,10 @@ public class SessionListener implements SessionDestroyListener {
@Override
public void sessionDestroy(SessionDestroyEvent event) {
- String username = (String) event.getSession().getAttribute("user");
NiederlassungInfo niederlassung = (NiederlassungInfo) event.getSession().getAttribute("niederlassung");
String sessionId = (String) event.getSession().getAttribute("sessionId");
- if (username != null && niederlassung != null) {
+ if (niederlassung != null) {
Application.activeNiederlassungen.remove(niederlassung.name());
}
if (sessionId != null) {
diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/SsoConfiguration.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/SsoConfiguration.java
new file mode 100644
index 0000000..b7705f4
--- /dev/null
+++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/SsoConfiguration.java
@@ -0,0 +1,18 @@
+package de.assecutor.emulatorstation.base.ui.view.security;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Service
+public class SsoConfiguration {
+
+ private final boolean enabled;
+
+ public SsoConfiguration(@Value("${app.sso.enabled:true}") boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+}
diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/VaadinAccessHandler.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/VaadinAccessHandler.java
deleted file mode 100644
index 3a37c6f..0000000
--- a/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/VaadinAccessHandler.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package de.assecutor.emulatorstation.base.ui.view.security;
-
-import com.vaadin.flow.component.UI;
-import com.vaadin.flow.router.BeforeEnterEvent;
-import com.vaadin.flow.server.VaadinService;
-import com.vaadin.flow.server.VaadinServiceInitListener;
-import com.vaadin.flow.shared.Registration;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-public class VaadinAccessHandler {
-
- @Bean
- public VaadinServiceInitListener registerSessionGuard() {
- return event -> event.getSource().addUIInitListener(uiInit -> {
- UI ui = uiInit.getUI();
- Registration reg = ui.addBeforeEnterListener(this::guardRoute);
- ui.addDetachListener(detach -> reg.remove());
- });
- }
-
- private void guardRoute(BeforeEnterEvent event) {
- var request = VaadinService.getCurrentRequest();
- if (request == null) {
- if (!event.getLocation().getPath().equals("main")) {
- event.rerouteTo("main");
- }
- return;
- }
-
- var session = request.getWrappedSession(false);
- if (session == null) {
- if (!event.getLocation().getPath().equals("main")) {
- event.rerouteTo("main");
- }
- return;
- }
-
- // Login-Route immer erlauben
- String path = event.getLocation().getPath();
- if (path.equals("login")) {
- return;
- }
-
- // Wenn keine Benutzer-Session vorhanden ist, immer zu "main"
- Object user = session.getAttribute("user");
- if (user == null) {
- if (!path.equals("main")) {
- event.rerouteTo("main");
- }
- }
- }
-}
diff --git a/src/main/java/de/assecutor/emulatorstation/pojo/CourierInfo.java b/src/main/java/de/assecutor/emulatorstation/pojo/CourierInfo.java
new file mode 100644
index 0000000..ddf0592
--- /dev/null
+++ b/src/main/java/de/assecutor/emulatorstation/pojo/CourierInfo.java
@@ -0,0 +1,11 @@
+package de.assecutor.emulatorstation.pojo;
+
+import java.io.Serializable;
+
+public record CourierInfo(long courierId, String courierSid, long userId, String userAccount,
+ String environment) implements Serializable {
+
+ public String displayLabel() {
+ return environment.toUpperCase() + " | " + courierSid + " | " + userAccount + " | User " + userId;
+ }
+}
diff --git a/src/main/java/de/assecutor/emulatorstation/pojo/SimCardInfo.java b/src/main/java/de/assecutor/emulatorstation/pojo/SimCardInfo.java
new file mode 100644
index 0000000..2e0fc9c
--- /dev/null
+++ b/src/main/java/de/assecutor/emulatorstation/pojo/SimCardInfo.java
@@ -0,0 +1,10 @@
+package de.assecutor.emulatorstation.pojo;
+
+import java.io.Serializable;
+
+public record SimCardInfo(long simCardId, long customerId, Long userId, String secretValue) implements Serializable {
+
+ public String displayLabel() {
+ return secretValue;
+ }
+}
diff --git a/src/main/java/util/PreferencesKeyValueStore.java b/src/main/java/util/PreferencesKeyValueStore.java
deleted file mode 100644
index 894f428..0000000
--- a/src/main/java/util/PreferencesKeyValueStore.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package util;
-
-import org.springframework.stereotype.Service;
-
-import java.util.Optional;
-import java.util.prefs.Preferences;
-
-@Service
-public class PreferencesKeyValueStore {
-
- private final Preferences prefs = Preferences.userRoot().node("emulatorstation");
-
- public void put(String key, String value) {
- prefs.put(key, value);
- }
-
- public Optional get(String key) {
- return Optional.ofNullable(prefs.get(key, null));
- }
-}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 9355b4b..b4794ca 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,5 +1,8 @@
server.port=${PORT:8080}
logging.level.org.atmosphere=warn
+spring.config.import=optional:file:./application-secrets.properties,optional:file:./.env[.properties]
+app.sso.enabled=${APP_SSO_ENABLED:true}
+app.emulator.server-ip=${EMULATOR_SERVER_IP:172.16.0.158}
# Launch the default browser when starting the application in development mode
vaadin.launch-browser=true
@@ -21,11 +24,12 @@ server.servlet.session.cookie.max-age=180d
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=false
server.servlet.session.persistent=true
+server.forward-headers-strategy=framework
# Vaadin session configuration
vaadin.heartbeatInterval=300
vaadin.closeIdleSessions=false
-# Disable Spring Boot's default generated user/password (we handle auth via Vaadin & custom login)
+# Disable Spring Boot's default generated user/password (auth is handled via Azure AD SSO)
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
diff --git a/src/test/java/de/assecutor/emulatorstation/base/domain/NiederlassungResolverTest.java b/src/test/java/de/assecutor/emulatorstation/base/domain/NiederlassungResolverTest.java
new file mode 100644
index 0000000..d52406d
--- /dev/null
+++ b/src/test/java/de/assecutor/emulatorstation/base/domain/NiederlassungResolverTest.java
@@ -0,0 +1,44 @@
+package de.assecutor.emulatorstation.base.domain;
+
+import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class NiederlassungResolverTest {
+
+ private final NiederlassungResolver niederlassungResolver = new NiederlassungResolver();
+
+ @Test
+ void resolvesNiederlassungFromKnownSimCodes() {
+ assertNiederlassung("EMU_B", "Berlin");
+ assertNiederlassung("EMU_HB", "Bremen");
+ assertNiederlassung("EMU_HH", "Hamburg");
+ assertNiederlassung("EMU_E", "Essen");
+ assertNiederlassung("EMU_L", "Leipzig");
+ assertNiederlassung("EMU_DD", "Dresden");
+ assertNiederlassung("EMU_H", "Hannover");
+ assertNiederlassung("EMU_S", "Stuttgart");
+ assertNiederlassung("EMU_F", "Frankfurt am Main");
+ assertNiederlassung("EMU_GFL", "Geschäftführung");
+ }
+
+ @Test
+ void resolvesSimCodesCaseInsensitive() {
+ assertEquals("HH", niederlassungResolver.extractCode("emu_hh"));
+ assertNiederlassung(" emu_b ", "Berlin");
+ }
+
+ @Test
+ void returnsNullForUnsupportedSimNames() {
+ assertNull(niederlassungResolver.extractCode("EMULATOR172_16_0_158"));
+ assertNull(niederlassungResolver.resolveFromSecretValue("EMU_X"));
+ assertNull(niederlassungResolver.resolveFromSecretValue(null));
+ }
+
+ private void assertNiederlassung(String secretValue, String expectedName) {
+ NiederlassungInfo niederlassungInfo = niederlassungResolver.resolveFromSecretValue(secretValue);
+ assertEquals(expectedName, niederlassungInfo.name());
+ }
+}