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()); + } +}