Compare commits
3 Commits
f764b4a7aa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b313cd588 | |||
| 9a67832faa | |||
| 4fc5b04a68 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,6 +4,8 @@
|
||||
.settings
|
||||
.project
|
||||
.classpath
|
||||
application-secrets.properties
|
||||
.env
|
||||
|
||||
*.iml
|
||||
.DS_Store
|
||||
|
||||
97
docker_push.sh
Executable file
97
docker_push.sh
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
readonly SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly PROJECT_DIR="${SCRIPT_DIR}"
|
||||
readonly DEFAULT_REGISTRY_IMAGE="registry.assecutor.org/emulatorstation"
|
||||
readonly REGISTRY_IMAGE="${REGISTRY_IMAGE:-${DEFAULT_REGISTRY_IMAGE}}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Verwendung:
|
||||
./docker_push.sh [x.y.z]
|
||||
|
||||
Beispiele:
|
||||
./docker_push.sh 0.9.13
|
||||
./docker_push.sh
|
||||
|
||||
Voraussetzungen:
|
||||
- Docker Buildx ist installiert
|
||||
- Login zur Registry wurde bereits ausgeführt:
|
||||
docker login registry.assecutor.org
|
||||
|
||||
Hinweise:
|
||||
- Ohne Versionsargument wird die Version aus der pom.xml als Docker-Tag verwendet.
|
||||
- Das JAR wird immer mit der aktuellen Projektversion aus der pom.xml gebaut.
|
||||
- Das Ziel-Image kann optional per Umgebungsvariable überschrieben werden:
|
||||
REGISTRY_IMAGE=registry.assecutor.org/mein-image ./docker_push.sh
|
||||
EOF
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "Fehler: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "'$1' wurde nicht gefunden."
|
||||
}
|
||||
|
||||
build_production_jar() {
|
||||
echo "Baue Production-JAR ${JAR_FILE_REL} ..."
|
||||
(
|
||||
cd "${PROJECT_DIR}" && ./mvnw -Pproduction -DskipTests clean package
|
||||
)
|
||||
|
||||
[[ -f "${JAR_FILE_ABS}" ]] || fail "Production-JAR wurde nicht gefunden: ${JAR_FILE_ABS}"
|
||||
}
|
||||
|
||||
resolve_pom_value() {
|
||||
local expression="$1"
|
||||
|
||||
[[ -x "${PROJECT_DIR}/mvnw" ]] || fail "'${PROJECT_DIR}/mvnw' wurde nicht gefunden oder ist nicht ausführbar."
|
||||
|
||||
local value
|
||||
value="$(
|
||||
cd "${PROJECT_DIR}" && ./mvnw -q -DforceStdout help:evaluate -Dexpression="${expression}" \
|
||||
| awk 'NF { last = $0 } END { print last }'
|
||||
)"
|
||||
|
||||
[[ -n "${value}" ]] || fail "Wert '${expression}' konnte nicht aus der pom.xml ermittelt werden."
|
||||
echo "${value}"
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
readonly PROJECT_VERSION="$(resolve_pom_value project.version)"
|
||||
readonly ARTIFACT_ID="$(resolve_pom_value project.artifactId)"
|
||||
readonly IMAGE_TAG="${1:-${PROJECT_VERSION}}"
|
||||
|
||||
if [[ ! "${IMAGE_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
fail "Versionsnummer muss das Format x.y.z haben."
|
||||
fi
|
||||
|
||||
require_command docker
|
||||
docker buildx version >/dev/null 2>&1 || fail "Docker Buildx ist nicht verfügbar."
|
||||
|
||||
readonly JAR_FILE_REL="target/${ARTIFACT_ID}-${PROJECT_VERSION}.jar"
|
||||
readonly JAR_FILE_ABS="${PROJECT_DIR}/${JAR_FILE_REL}"
|
||||
|
||||
cd "${PROJECT_DIR}"
|
||||
|
||||
echo "Verwende Build-Version ${PROJECT_VERSION} und Image-Tag ${IMAGE_TAG}."
|
||||
build_production_jar
|
||||
|
||||
echo "Pushe Image ${REGISTRY_IMAGE}:${IMAGE_TAG} ..."
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
-f "${PROJECT_DIR}/Dockerfile" \
|
||||
-t "${REGISTRY_IMAGE}:${IMAGE_TAG}" \
|
||||
--push \
|
||||
"${PROJECT_DIR}"
|
||||
|
||||
echo "Fertig: ${REGISTRY_IMAGE}:${IMAGE_TAG}"
|
||||
6
pom.xml
6
pom.xml
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>de.assecutor.emulatorstation</groupId>
|
||||
<artifactId>emulatorstation</artifactId>
|
||||
<version>0.9.13</version>
|
||||
<version>0.9.2</version>
|
||||
|
||||
<packaging>jar</packaging>
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
|
||||
Binary file not shown.
@@ -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<String, NiederlassungInfo> 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<String, com.vaadin.flow.server.WrappedSession> sessionsById = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -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<String> 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<String> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
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<CourierInfo> fetchCouriers() {
|
||||
try {
|
||||
HttpResponse<String> response = sendGet("/api/couriers");
|
||||
JsonNode root = objectMapper.readTree(response.body());
|
||||
List<CourierInfo> 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<SimCardInfo> fetchSimCards() {
|
||||
try {
|
||||
HttpResponse<String> response = sendGet("/api/simcards");
|
||||
JsonNode root = objectMapper.readTree(response.body());
|
||||
List<SimCardInfo> 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 releaseSimCard(SimCardInfo simCard) {
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(buildUri("/api/simcards/" + simCard.simCardId() + "/assign")).DELETE().build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
ensureSuccessful(response, "Zuordnung der SIM-Karte konnte nicht aufgehoben werden.");
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Zuordnung der SIM-Karte konnte nicht aufgehoben werden.", e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Aufheben der Zuordnung 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<String> 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<CourierInfo> 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<String> sendGet(String path) throws IOException, InterruptedException {
|
||||
HttpRequest request = HttpRequest.newBuilder().uri(buildUri(path)).GET().build();
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
ensureSuccessful(response, "API-Daten konnten nicht geladen werden.");
|
||||
return response;
|
||||
}
|
||||
|
||||
private void ensureSuccessful(HttpResponse<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -37,13 +40,18 @@ import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@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 +63,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 +80,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 +161,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 +237,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 +264,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 +275,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 +295,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() {
|
||||
@@ -310,6 +337,8 @@ public final class MainView extends Main implements BeforeEnterObserver {
|
||||
downloadApp();
|
||||
|
||||
installApp();
|
||||
|
||||
startApp();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
// Logging, Fehlerbehandlung …
|
||||
@@ -390,6 +419,83 @@ public final class MainView extends Main implements BeforeEnterObserver {
|
||||
}
|
||||
}
|
||||
|
||||
private void startApp() {
|
||||
String packageName = listThirdPartyPackage();
|
||||
if (packageName == null || packageName.isBlank()) {
|
||||
logger.error("Konnte Package-Namen der installierten App nicht ermitteln - App wird nicht gestartet");
|
||||
return;
|
||||
}
|
||||
logger.info("Starte App mit Package-Name: {}", packageName);
|
||||
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
|
||||
String jsonPayload = """
|
||||
{
|
||||
"Cmd": ["adb", "shell", "monkey", "-p", "%s", "-c", "android.intent.category.LAUNCHER", "1"],
|
||||
"AttachStdout": true,
|
||||
"AttachStderr": true
|
||||
}""".formatted(packageName);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(
|
||||
"http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/exec"))
|
||||
.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
||||
.build();
|
||||
|
||||
try {
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
logger.info("HTTP-Response-Code: {}", response.statusCode());
|
||||
logger.info("Response-Body: {}", response.body());
|
||||
|
||||
var execId = ExecResponse.parse(response.body());
|
||||
|
||||
exec(execId);
|
||||
} catch (Exception e) {
|
||||
logger.error("Fehler beim Starten der App", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String listThirdPartyPackage() {
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
|
||||
String jsonPayload = """
|
||||
{
|
||||
"Cmd": ["adb", "shell", "pm", "list", "packages", "-3"],
|
||||
"AttachStdout": true,
|
||||
"AttachStderr": true
|
||||
}""";
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(
|
||||
"http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/exec"))
|
||||
.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
||||
.build();
|
||||
|
||||
try {
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
logger.info("HTTP-Response-Code: {}", response.statusCode());
|
||||
logger.info("Response-Body: {}", response.body());
|
||||
|
||||
var execId = ExecResponse.parse(response.body());
|
||||
var execResponse = exec(execId);
|
||||
return extractFirstPackageName(execResponse);
|
||||
} catch (Exception e) {
|
||||
logger.error("Fehler beim Ermitteln des Package-Namens", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String extractFirstPackageName(String body) {
|
||||
if (body == null) {
|
||||
return null;
|
||||
}
|
||||
Matcher matcher = Pattern.compile("package:([A-Za-z0-9_.]+)").matcher(body);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void installApp() {
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
|
||||
@@ -609,6 +715,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 +725,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<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
int status = response.statusCode();
|
||||
logger.info("Inspect-Container '{}': status={} ", name, status);
|
||||
@@ -645,7 +750,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 +839,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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
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.html.Span;
|
||||
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.data.renderer.ComponentRenderer;
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Route("sim-config")
|
||||
@PageTitle("SIM-Konfiguration")
|
||||
@AnonymousAllowed
|
||||
public class SimCardConfigurationView extends VerticalLayout implements BeforeEnterObserver {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SimCardConfigurationView.class);
|
||||
|
||||
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<CourierInfo> courierSelect = new ComboBox<>("Kurier");
|
||||
private final ComboBox<SimCardInfo> simCardSelect = new ComboBox<>("SIM-Karte");
|
||||
private final Paragraph statusText = new Paragraph("Lade Kuriere und SIM-Karten...");
|
||||
private final Button assignButton;
|
||||
private final Map<Long, String> courierSidByUserId = new HashMap<>();
|
||||
|
||||
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);
|
||||
simCardSelect.setRenderer(new ComponentRenderer<>(simCard -> {
|
||||
HorizontalLayout row = new HorizontalLayout();
|
||||
row.setAlignItems(Alignment.CENTER);
|
||||
row.setSpacing(true);
|
||||
row.setPadding(false);
|
||||
row.setWidthFull();
|
||||
row.setJustifyContentMode(JustifyContentMode.BETWEEN);
|
||||
|
||||
Span label = new Span(simCard.displayLabel());
|
||||
row.add(label);
|
||||
|
||||
if (isSimCardOccupied(simCard)) {
|
||||
String courierSid = courierSidByUserId.getOrDefault(simCard.userId(),
|
||||
String.valueOf(simCard.userId()));
|
||||
Span badge = new Span("Belegt durch " + courierSid);
|
||||
badge.getElement().getThemeList().add("badge error small");
|
||||
|
||||
Button releaseButton = new Button(new Icon(VaadinIcon.UNLINK));
|
||||
releaseButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE, ButtonVariant.LUMO_SMALL,
|
||||
ButtonVariant.LUMO_ERROR);
|
||||
releaseButton.getElement().setAttribute("title", "Zuordnung aufheben");
|
||||
releaseButton.addClickListener(event -> confirmReleaseSimCard(simCard));
|
||||
releaseButton.getElement().addEventListener("click", event -> {
|
||||
}).addEventData("event.stopPropagation()");
|
||||
|
||||
HorizontalLayout actionsLayout = new HorizontalLayout(badge, releaseButton);
|
||||
actionsLayout.setAlignItems(Alignment.CENTER);
|
||||
actionsLayout.setSpacing(true);
|
||||
actionsLayout.setPadding(false);
|
||||
row.add(actionsLayout);
|
||||
}
|
||||
return row;
|
||||
}));
|
||||
|
||||
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<CourierInfo> couriers = simCardAssignmentService.fetchCouriers();
|
||||
List<SimCardInfo> simCards = simCardAssignmentService.fetchSimCards();
|
||||
|
||||
courierSidByUserId.clear();
|
||||
for (CourierInfo courier : couriers) {
|
||||
courierSidByUserId.putIfAbsent(courier.userId(), courier.courierSid());
|
||||
}
|
||||
|
||||
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 {
|
||||
logger.info("Sende SIM-Zuordnung: Kurier='{}', SIM-Karte='{}'", courier.displayLabel(),
|
||||
simCard.displayLabel());
|
||||
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);
|
||||
}
|
||||
|
||||
private void confirmReleaseSimCard(SimCardInfo simCard) {
|
||||
simCardSelect.getElement().executeJs("this.close()");
|
||||
|
||||
Dialog dialog = new Dialog();
|
||||
dialog.setHeaderTitle("Zuordnung aufheben");
|
||||
|
||||
Paragraph text = new Paragraph(
|
||||
"Möchten Sie die Zuordnung der SIM-Karte '" + simCard.displayLabel() + "' wirklich aufheben?");
|
||||
|
||||
Button confirmButton = new Button("Aufheben", event -> {
|
||||
dialog.close();
|
||||
releaseSimCard(simCard);
|
||||
});
|
||||
confirmButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
|
||||
|
||||
Button cancelButton = new Button("Abbrechen", event -> dialog.close());
|
||||
|
||||
HorizontalLayout buttons = new HorizontalLayout(cancelButton, confirmButton);
|
||||
buttons.setWidthFull();
|
||||
buttons.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
|
||||
|
||||
dialog.add(text, buttons);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
private void releaseSimCard(SimCardInfo simCard) {
|
||||
try {
|
||||
simCardAssignmentService.releaseSimCard(simCard);
|
||||
Notification.show("Zuordnung der SIM-Karte '" + simCard.displayLabel() + "' wurde aufgehoben.", 3000,
|
||||
Notification.Position.MIDDLE);
|
||||
loadOptions();
|
||||
} catch (Exception ex) {
|
||||
showError(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isSimCardOccupied(SimCardInfo simCard) {
|
||||
Long userId = simCard.userId();
|
||||
return userId != null && userId != 0L;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package de.assecutor.emulatorstation.base.ui.view.security;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Condition;
|
||||
import org.springframework.context.annotation.ConditionContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class AzureClientRegistrationConfiguration {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AzureClientRegistrationConfiguration.class);
|
||||
|
||||
private static final String DEFAULT_REDIRECT_URI = "{baseUrl}/login/oauth2/code/{registrationId}";
|
||||
private static final String DEFAULT_SCOPES = "openid,profile,email,offline_access";
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ClientRegistrationRepository.class)
|
||||
@org.springframework.context.annotation.Conditional(AzureClientRegistrationAvailableCondition.class)
|
||||
public ClientRegistrationRepository azureClientRegistrationRepository(Environment environment) {
|
||||
String tenantId = resolveTenantId(environment);
|
||||
String issuerUri = resolveIssuerUri(environment, tenantId);
|
||||
String clientId = environment.getProperty("AZURE_CLIENT_ID");
|
||||
String clientSecret = environment.getProperty("AZURE_CLIENT_SECRET");
|
||||
String redirectUri = firstNonBlank(environment.getProperty("AZURE_REDIRECT_URI"), DEFAULT_REDIRECT_URI);
|
||||
List<String> scopes = parseScopes(firstNonBlank(environment.getProperty("AZURE_SCOPES"), DEFAULT_SCOPES));
|
||||
|
||||
ClientRegistration registration = ClientRegistration.withRegistrationId("azure").clientName("Azure")
|
||||
.clientId(clientId).clientSecret(clientSecret)
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).redirectUri(redirectUri)
|
||||
.scope(scopes).authorizationUri(buildAuthorizationUri(tenantId)).tokenUri(buildTokenUri(tenantId))
|
||||
.jwkSetUri(buildJwkSetUri(tenantId)).issuerUri(issuerUri).userNameAttributeName(IdTokenClaimNames.SUB)
|
||||
.build();
|
||||
|
||||
logger.info("Azure OAuth2 ClientRegistration wurde aus AZURE_* Umgebungsvariablen erstellt.");
|
||||
return new InMemoryClientRegistrationRepository(registration);
|
||||
}
|
||||
|
||||
static String resolveTenantId(Environment environment) {
|
||||
String tenantId = environment.getProperty("AZURE_TENANT_ID");
|
||||
if (StringUtils.hasText(tenantId)) {
|
||||
return tenantId.trim();
|
||||
}
|
||||
|
||||
String issuerUri = environment.getProperty("AZURE_ISSUER_URI");
|
||||
if (!StringUtils.hasText(issuerUri)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalized = issuerUri.trim();
|
||||
String marker = "login.microsoftonline.com/";
|
||||
int markerIndex = normalized.indexOf(marker);
|
||||
if (markerIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String afterHost = normalized.substring(markerIndex + marker.length());
|
||||
int slashIndex = afterHost.indexOf('/');
|
||||
if (slashIndex < 0) {
|
||||
return StringUtils.hasText(afterHost) ? afterHost : null;
|
||||
}
|
||||
|
||||
String tenant = afterHost.substring(0, slashIndex);
|
||||
return StringUtils.hasText(tenant) ? tenant : null;
|
||||
}
|
||||
|
||||
static String resolveIssuerUri(Environment environment, String tenantId) {
|
||||
String issuerUri = environment.getProperty("AZURE_ISSUER_URI");
|
||||
if (StringUtils.hasText(issuerUri)) {
|
||||
return issuerUri.trim();
|
||||
}
|
||||
if (!StringUtils.hasText(tenantId)) {
|
||||
return null;
|
||||
}
|
||||
return "https://login.microsoftonline.com/" + tenantId + "/v2.0";
|
||||
}
|
||||
|
||||
private static String buildAuthorizationUri(String tenantId) {
|
||||
return "https://login.microsoftonline.com/" + tenantId + "/oauth2/v2.0/authorize";
|
||||
}
|
||||
|
||||
private static String buildTokenUri(String tenantId) {
|
||||
return "https://login.microsoftonline.com/" + tenantId + "/oauth2/v2.0/token";
|
||||
}
|
||||
|
||||
private static String buildJwkSetUri(String tenantId) {
|
||||
return "https://login.microsoftonline.com/" + tenantId + "/discovery/v2.0/keys";
|
||||
}
|
||||
|
||||
private static List<String> parseScopes(String scopesValue) {
|
||||
return Arrays.stream(scopesValue.split(",")).map(String::trim).filter(StringUtils::hasText).toList();
|
||||
}
|
||||
|
||||
private static String firstNonBlank(String value, String fallback) {
|
||||
return StringUtils.hasText(value) ? value.trim() : fallback;
|
||||
}
|
||||
|
||||
static final class AzureClientRegistrationAvailableCondition implements Condition {
|
||||
|
||||
@Override
|
||||
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||
Environment environment = context.getEnvironment();
|
||||
return StringUtils.hasText(environment.getProperty("AZURE_CLIENT_ID"))
|
||||
&& StringUtils.hasText(environment.getProperty("AZURE_CLIENT_SECRET"))
|
||||
&& StringUtils.hasText(resolveTenantId(environment));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -2,34 +2,73 @@ package de.assecutor.emulatorstation.base.ui.view.security;
|
||||
|
||||
import com.vaadin.flow.spring.security.VaadinWebSecurity;
|
||||
import de.assecutor.emulatorstation.base.ui.view.LoginView;
|
||||
import de.assecutor.emulatorstation.base.ui.view.SimCardConfigurationView;
|
||||
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;
|
||||
private final SsoConfiguration ssoConfiguration;
|
||||
|
||||
public SecurityConfig(LoginSessionService loginSessionService, SsoConfiguration ssoConfiguration) {
|
||||
this.loginSessionService = loginSessionService;
|
||||
this.ssoConfiguration = ssoConfiguration;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
super.configure(http);
|
||||
|
||||
setLoginView(http, LoginView.class);
|
||||
if (ssoConfiguration.isEnabled()) {
|
||||
setLoginView(http, LoginView.class);
|
||||
} else {
|
||||
setLoginView(http, SimCardConfigurationView.class);
|
||||
}
|
||||
|
||||
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)
|
||||
.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));
|
||||
.sessionFixation(fixation -> fixation.migrateSession()).maximumSessions(Application.MAX_ACTIVE_SESSIONS)
|
||||
.maxSessionsPreventsLogin(false));
|
||||
|
||||
if (ssoConfiguration.isEnabled()) {
|
||||
LogoutSuccessHandler logoutSuccessHandler = oidcLogoutSuccessHandler("{baseUrl}/login");
|
||||
|
||||
http.oauth2Login(
|
||||
oauth2 -> oauth2.loginPage("/login").defaultSuccessUrl("/login", true).failureUrl("/login?error"));
|
||||
|
||||
http.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");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
http.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());
|
||||
}
|
||||
}));
|
||||
}).logoutSuccessUrl("/"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package de.assecutor.emulatorstation.base.ui.view.security;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class SsoConfiguration {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SsoConfiguration.class);
|
||||
|
||||
private final boolean enabled;
|
||||
|
||||
public SsoConfiguration(@Value("${app.sso.enabled:true}") boolean enabled,
|
||||
ObjectProvider<ClientRegistrationRepository> clientRegistrationRepositoryProvider) {
|
||||
boolean clientRegistrationAvailable = clientRegistrationRepositoryProvider.getIfAvailable() != null;
|
||||
this.enabled = enabled && clientRegistrationAvailable;
|
||||
|
||||
if (enabled && !clientRegistrationAvailable) {
|
||||
logger.warn("SSO ist aktiviert, aber keine OAuth2-Client-Registrierung verfuegbar. "
|
||||
+ "Die Anwendung startet daher mit deaktiviertem SSO.");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<String> get(String key) {
|
||||
return Optional.ofNullable(prefs.get(key, null));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user