Compare commits
3 Commits
f764b4a7aa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b313cd588 | |||
| 9a67832faa | |||
| 4fc5b04a68 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,6 +4,8 @@
|
|||||||
.settings
|
.settings
|
||||||
.project
|
.project
|
||||||
.classpath
|
.classpath
|
||||||
|
application-secrets.properties
|
||||||
|
.env
|
||||||
|
|
||||||
*.iml
|
*.iml
|
||||||
.DS_Store
|
.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>
|
<groupId>de.assecutor.emulatorstation</groupId>
|
||||||
<artifactId>emulatorstation</artifactId>
|
<artifactId>emulatorstation</artifactId>
|
||||||
<version>0.9.13</version>
|
<version>0.9.2</version>
|
||||||
|
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
@@ -75,6 +75,10 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-security</artifactId>
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-oauth2-client</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-validation</artifactId>
|
<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.AppShellConfigurator;
|
||||||
import com.vaadin.flow.component.page.Push;
|
import com.vaadin.flow.component.page.Push;
|
||||||
import com.vaadin.flow.theme.Theme;
|
import com.vaadin.flow.theme.Theme;
|
||||||
|
import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import de.assecutor.emulatorstation.pojo.UserInfo;
|
|
||||||
import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
|
|
||||||
|
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@Push
|
@Push
|
||||||
@@ -18,9 +17,6 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
|||||||
|
|
||||||
@Theme("default")
|
@Theme("default")
|
||||||
public class Application implements AppShellConfigurator {
|
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 int MAX_ACTIVE_SESSIONS = 5;
|
||||||
|
|
||||||
public static final Map<String, NiederlassungInfo> niederlassungen = Map.ofEntries(
|
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("Dresden", new NiederlassungInfo("Dresden", "172.18.0.106", "6086", "/dresden")),
|
||||||
Map.entry("Hannover", new NiederlassungInfo("Hannover", "172.18.0.104", "6084", "/hannover")),
|
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("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")));
|
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<>();
|
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;
|
package de.assecutor.emulatorstation.base.domain;
|
||||||
|
|
||||||
import de.assecutor.emulatorstation.Application;
|
import de.assecutor.emulatorstation.Application;
|
||||||
|
import de.assecutor.emulatorstation.base.ui.view.security.EmulatorServerConfiguration;
|
||||||
import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
|
import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -17,9 +18,12 @@ public class ContainerShutdownScheduler {
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ContainerShutdownScheduler.class);
|
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 HttpClient http = HttpClient.newHttpClient();
|
||||||
|
private final EmulatorServerConfiguration emulatorServerConfiguration;
|
||||||
|
|
||||||
|
public ContainerShutdownScheduler(EmulatorServerConfiguration emulatorServerConfiguration) {
|
||||||
|
this.emulatorServerConfiguration = emulatorServerConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
// Täglich um 22:00 Uhr Serverzeit
|
// Täglich um 22:00 Uhr Serverzeit
|
||||||
@Scheduled(cron = "0 0 22 * * *")
|
@Scheduled(cron = "0 0 22 * * *")
|
||||||
@@ -40,8 +44,10 @@ public class ContainerShutdownScheduler {
|
|||||||
logger.info("[Scheduler] Nächtliches Herunterfahren abgeschlossen");
|
logger.info("[Scheduler] Nächtliches Herunterfahren abgeschlossen");
|
||||||
|
|
||||||
// Alle Sessions beenden (Logout erzwingen)
|
// Alle Sessions beenden (Logout erzwingen)
|
||||||
logger.info("[Scheduler] Beginne Session-Logout für alle aktiven Sessions: {}", Application.sessionsById.size());
|
logger.info("[Scheduler] Beginne Session-Logout für alle aktiven Sessions: {}",
|
||||||
for (com.vaadin.flow.server.WrappedSession session : new java.util.ArrayList<>(Application.sessionsById.values())) {
|
Application.sessionsById.size());
|
||||||
|
for (com.vaadin.flow.server.WrappedSession session : new java.util.ArrayList<>(
|
||||||
|
Application.sessionsById.values())) {
|
||||||
try {
|
try {
|
||||||
session.invalidate();
|
session.invalidate();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -52,7 +58,8 @@ public class ContainerShutdownScheduler {
|
|||||||
Application.activeSessions.clear();
|
Application.activeSessions.clear();
|
||||||
Application.activeNiederlassungen.clear();
|
Application.activeNiederlassungen.clear();
|
||||||
Application.sessionsById.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 {
|
private boolean isRunning(String containerName) throws Exception {
|
||||||
|
String dockerHost = emulatorServerConfiguration.getServerIp();
|
||||||
HttpRequest req = HttpRequest.newBuilder()
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
.uri(URI.create("http://" + DOCKER_HOST + ":2375/containers/" + containerName + "/json"))
|
.uri(URI.create("http://" + dockerHost + ":2375/containers/" + containerName + "/json")).GET().build();
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
if (resp.statusCode() == 200) {
|
if (resp.statusCode() == 200) {
|
||||||
String body = resp.body();
|
String body = resp.body();
|
||||||
@@ -81,10 +87,10 @@ public class ContainerShutdownScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void stopContainer(String containerName) throws Exception {
|
private void stopContainer(String containerName) throws Exception {
|
||||||
|
String dockerHost = emulatorServerConfiguration.getServerIp();
|
||||||
HttpRequest req = HttpRequest.newBuilder()
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
.uri(URI.create("http://" + DOCKER_HOST + ":2375/containers/" + containerName + "/stop"))
|
.uri(URI.create("http://" + dockerHost + ":2375/containers/" + containerName + "/stop"))
|
||||||
.POST(HttpRequest.BodyPublishers.noBody())
|
.POST(HttpRequest.BodyPublishers.noBody()).build();
|
||||||
.build();
|
|
||||||
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
int status = resp.statusCode();
|
int status = resp.statusCode();
|
||||||
if (status == 204 || status == 304) {
|
if (status == 204 || status == 304) {
|
||||||
@@ -92,8 +98,8 @@ public class ContainerShutdownScheduler {
|
|||||||
} else if (status == 404) {
|
} else if (status == 404) {
|
||||||
logger.info("[Scheduler] Container '{}' nicht gefunden (404)", containerName);
|
logger.info("[Scheduler] Container '{}' nicht gefunden (404)", containerName);
|
||||||
} else {
|
} 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;
|
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.Button;
|
||||||
import com.vaadin.flow.component.button.ButtonVariant;
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
import com.vaadin.flow.component.textfield.PasswordField;
|
import com.vaadin.flow.component.html.Div;
|
||||||
import com.vaadin.flow.component.notification.Notification;
|
import com.vaadin.flow.component.html.H1;
|
||||||
import com.vaadin.flow.component.icon.Icon;
|
import com.vaadin.flow.component.icon.Icon;
|
||||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
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.router.Route;
|
||||||
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
||||||
import de.assecutor.emulatorstation.Application;
|
import com.vaadin.flow.spring.security.AuthenticationContext;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import de.assecutor.emulatorstation.base.ui.view.security.LoginSessionService;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import de.assecutor.emulatorstation.base.ui.view.security.SsoConfiguration;
|
||||||
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;
|
|
||||||
|
|
||||||
|
|
||||||
@Route("login")
|
@Route("login")
|
||||||
@AnonymousAllowed
|
@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();
|
setSizeFull();
|
||||||
setAlignItems(Alignment.CENTER);
|
setAlignItems(Alignment.CENTER);
|
||||||
setJustifyContentMode(JustifyContentMode.CENTER);
|
setJustifyContentMode(JustifyContentMode.CENTER);
|
||||||
getStyle().set("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)").set("min-height", "100vh")
|
getStyle().set("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)").set("min-height", "100vh")
|
||||||
.set("padding", "20px");
|
.set("padding", "20px");
|
||||||
|
|
||||||
// Haupt-Container für das Login-Formular
|
|
||||||
Div loginContainer = new Div();
|
Div loginContainer = new Div();
|
||||||
loginContainer.getStyle().set("background-color", "#ffffff").set("border-radius", "12px")
|
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");
|
.set("width", "100%").set("text-align", "center");
|
||||||
|
|
||||||
// Logo/Icon
|
|
||||||
Icon icon = new Icon(VaadinIcon.DESKTOP);
|
Icon icon = new Icon(VaadinIcon.DESKTOP);
|
||||||
icon.setSize("64px");
|
icon.setSize("64px");
|
||||||
icon.getStyle().set("color", "#667eea").set("margin-bottom", "20px");
|
icon.getStyle().set("color", "#667eea").set("margin-bottom", "20px");
|
||||||
|
|
||||||
// Titel
|
|
||||||
H1 title = new H1("Emulator Station");
|
H1 title = new H1("Emulator Station");
|
||||||
title.getStyle().set("margin", "0 0 8px 0").set("color", "#333333").set("font-size", "2rem").set("font-weight",
|
title.getStyle().set("margin", "0 0 8px 0").set("color", "#333333").set("font-size", "2rem").set("font-weight",
|
||||||
"600");
|
"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();
|
VerticalLayout formLayout = new VerticalLayout();
|
||||||
formLayout.setSpacing(true);
|
formLayout.setSpacing(true);
|
||||||
formLayout.setPadding(false);
|
formLayout.setPadding(false);
|
||||||
formLayout.setWidthFull();
|
formLayout.setWidthFull();
|
||||||
|
|
||||||
// Eingabefelder - nur noch Passwort
|
Button loginButton = new Button("Mit Microsoft anmelden", new Icon(VaadinIcon.SIGN_IN), event -> startLogin());
|
||||||
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
|
|
||||||
loginButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE);
|
loginButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE);
|
||||||
loginButton.setWidthFull();
|
loginButton.setWidthFull();
|
||||||
loginButton.getStyle().set("margin-top", "8px")
|
loginButton.getStyle().set("margin-top", "8px")
|
||||||
.set("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)").set("border", "none")
|
.set("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)").set("border", "none")
|
||||||
.set("font-weight", "600");
|
.set("font-weight", "600");
|
||||||
|
|
||||||
// Eingabefelder zum Form-Layout hinzufügen
|
formLayout.add(loginButton);
|
||||||
formLayout.add(passwordField, niederlassungSelect, loginButton);
|
loginContainer.add(icon, title, formLayout);
|
||||||
|
|
||||||
// Alle Komponenten zum Login-Container hinzufügen
|
|
||||||
loginContainer.add(icon, title, subtitle, formLayout);
|
|
||||||
|
|
||||||
// Login-Container zur Hauptansicht hinzufügen
|
|
||||||
add(loginContainer);
|
add(loginContainer);
|
||||||
|
|
||||||
if (MainLayout.instance != null) {
|
if (MainLayout.instance != null) {
|
||||||
@@ -149,78 +70,39 @@ public class LoginView extends VerticalLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isContainerRunning(de.assecutor.emulatorstation.pojo.NiederlassungInfo info) {
|
@Override
|
||||||
if (info == null) {
|
public void afterNavigation(AfterNavigationEvent event) {
|
||||||
return false;
|
getUI().ifPresent(ui -> {
|
||||||
}
|
if (!ssoConfiguration.isEnabled()) {
|
||||||
final String srv = "172.16.0.158"; // Fallback wie in MainView
|
ui.navigate(SimCardConfigurationView.class);
|
||||||
final String name = "android-container-" + info.name();
|
return;
|
||||||
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");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("Fehler beim Pruefen des Container-Status fuer '{}': {}", name, e.toString());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void completeLogin(com.vaadin.flow.component.UI ui, String password,
|
if (authenticationContext.isAuthenticated()) {
|
||||||
de.assecutor.emulatorstation.pojo.NiederlassungInfo niederlassungInfo,
|
if (loginSessionService.hasApplicationSession(ui.getSession())
|
||||||
boolean adoptRunningContainer) {
|
&& loginSessionService.hasCompletedSimConfiguration(ui.getSession())) {
|
||||||
String niederlassung = niederlassungInfo.name();
|
ui.navigate(MainView.class);
|
||||||
|
} else {
|
||||||
String sessionId = ui.getSession().getSession().getId();
|
ui.navigate(SimCardConfigurationView.class);
|
||||||
// Session niemals ablaufen lassen
|
}
|
||||||
ui.getSession().getSession().setMaxInactiveInterval(-1);
|
return;
|
||||||
|
|
||||||
// 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:");
|
if (event.getLocation().getQueryParameters().getParameters().containsKey("error")) {
|
||||||
logger.info("Username: {}", Application.SINGLE_USERNAME);
|
Notification.show("Die Microsoft-Anmeldung konnte nicht abgeschlossen werden.", 5000,
|
||||||
logger.info("Niederlassung: {}", niederlassungInfo.name());
|
Notification.Position.MIDDLE);
|
||||||
logger.info("SessionId: {}", sessionId);
|
}
|
||||||
logger.info("Aktive Sessions: {}/{}", Application.activeSessions.size(), Application.MAX_ACTIVE_SESSIONS);
|
});
|
||||||
|
|
||||||
ui.navigate("main");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void startLogin() {
|
||||||
|
getUI().ifPresent(ui -> {
|
||||||
|
if (!ssoConfiguration.isEnabled() || authenticationContext.isAuthenticated()) {
|
||||||
|
ui.navigate(SimCardConfigurationView.class);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.component.sidenav.SideNavItem;
|
||||||
import com.vaadin.flow.router.Layout;
|
import com.vaadin.flow.router.Layout;
|
||||||
import com.vaadin.flow.server.VaadinSession;
|
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.MenuConfiguration;
|
||||||
import com.vaadin.flow.server.menu.MenuEntry;
|
import com.vaadin.flow.server.menu.MenuEntry;
|
||||||
import jakarta.annotation.security.PermitAll;
|
|
||||||
|
|
||||||
import static com.vaadin.flow.theme.lumo.LumoUtility.*;
|
import static com.vaadin.flow.theme.lumo.LumoUtility.*;
|
||||||
|
|
||||||
@Layout
|
@Layout
|
||||||
@PermitAll // When security is enabled, allow all authenticated users
|
@AnonymousAllowed
|
||||||
public final class MainLayout extends AppLayout {
|
public final class MainLayout extends AppLayout {
|
||||||
public static MainLayout instance = null;
|
public static MainLayout instance = null;
|
||||||
|
|
||||||
@@ -54,7 +54,6 @@ public final class MainLayout extends AppLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Div createHeader() {
|
private Div createHeader() {
|
||||||
// TODO Replace with real application logo and name
|
|
||||||
var appLogo = VaadinIcon.CUBES.create();
|
var appLogo = VaadinIcon.CUBES.create();
|
||||||
appLogo.addClassNames(TextColor.PRIMARY, IconSize.LARGE);
|
appLogo.addClassNames(TextColor.PRIMARY, IconSize.LARGE);
|
||||||
|
|
||||||
@@ -82,7 +81,6 @@ public final class MainLayout extends AppLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Component createUserMenu() {
|
private Component createUserMenu() {
|
||||||
// TODO Replace with real user information and actions
|
|
||||||
var avatar = new Avatar("John Smith");
|
var avatar = new Avatar("John Smith");
|
||||||
avatar.addThemeVariants(AvatarVariant.LUMO_XSMALL);
|
avatar.addThemeVariants(AvatarVariant.LUMO_XSMALL);
|
||||||
avatar.addClassNames(Margin.Right.SMALL);
|
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.Route;
|
||||||
import com.vaadin.flow.router.PreserveOnRefresh;
|
import com.vaadin.flow.router.PreserveOnRefresh;
|
||||||
|
|
||||||
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
|
||||||
import com.vaadin.flow.server.VaadinSession;
|
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.ExecResponse;
|
||||||
import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
|
import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
|
||||||
import de.assecutor.emulatorstation.Application;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -37,13 +40,18 @@ import java.net.http.HttpRequest;
|
|||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
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")
|
@Route("main")
|
||||||
@PreserveOnRefresh
|
@PreserveOnRefresh
|
||||||
@AnonymousAllowed
|
|
||||||
public final class MainView extends Main implements BeforeEnterObserver {
|
public final class MainView extends Main implements BeforeEnterObserver {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(MainView.class);
|
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 String username;
|
||||||
private NiederlassungInfo niederlassung;
|
private NiederlassungInfo niederlassung;
|
||||||
private String server;
|
private String server;
|
||||||
@@ -55,7 +63,13 @@ public final class MainView extends Main implements BeforeEnterObserver {
|
|||||||
|
|
||||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
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();
|
setSizeFull();
|
||||||
getStyle().set("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)").set("min-height", "100vh")
|
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")
|
.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")
|
contentContainer.getStyle().set("width", "95%").set("height", "95%").set("display", "flex")
|
||||||
.set("flex-direction", "column").set("box-sizing", "border-box");
|
.set("flex-direction", "column").set("box-sizing", "border-box");
|
||||||
|
|
||||||
|
|
||||||
setupWelcomeMessage(contentContainer);
|
setupWelcomeMessage(contentContainer);
|
||||||
setupEmulatorContainer(contentContainer);
|
setupEmulatorContainer(contentContainer);
|
||||||
setupButtonLayout(contentContainer);
|
setupButtonLayout(contentContainer);
|
||||||
@@ -148,8 +161,6 @@ public final class MainView extends Main implements BeforeEnterObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void setupWelcomeMessage(Div container) {
|
private void setupWelcomeMessage(Div container) {
|
||||||
welcomeMessage.addClassName("welcome-panel");
|
welcomeMessage.addClassName("welcome-panel");
|
||||||
welcomeMessage.getStyle().set("display", "flex").set("flex-direction", "column").set("align-items", "center")
|
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();
|
var vaadinSession = currentUI.get().getSession();
|
||||||
username = (String) vaadinSession.getAttribute("username");
|
username = (String) vaadinSession.getAttribute("username");
|
||||||
niederlassung = (NiederlassungInfo) vaadinSession.getAttribute("niederlassung");
|
niederlassung = (NiederlassungInfo) vaadinSession.getAttribute("niederlassung");
|
||||||
server = (String) vaadinSession.getAttribute("server");
|
|
||||||
|
|
||||||
// Fallback: Nach Reload Niederlassung anhand der aktiven Sessions rekonstruieren
|
// Fallback: Nach Reload Niederlassung anhand der aktiven Sessions rekonstruieren
|
||||||
if (niederlassung == null) {
|
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");
|
logger.error("UI nicht verfügbar - kann Session-Daten nicht laden");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server == null) {
|
server = emulatorServerConfiguration.getServerIp();
|
||||||
server = "172.16.0.158";
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("MainView Session-Daten geladen:");
|
logger.info("MainView Session-Daten geladen:");
|
||||||
logger.info("Username: {}", username);
|
logger.info("Username: {}", username);
|
||||||
@@ -267,7 +275,8 @@ public final class MainView extends Main implements BeforeEnterObserver {
|
|||||||
|
|
||||||
private void restoreUIStateIfNeeded() {
|
private void restoreUIStateIfNeeded() {
|
||||||
var uiOpt = getUI();
|
var uiOpt = getUI();
|
||||||
if (uiOpt.isEmpty()) return;
|
if (uiOpt.isEmpty())
|
||||||
|
return;
|
||||||
var session = uiOpt.get().getSession();
|
var session = uiOpt.get().getSession();
|
||||||
|
|
||||||
boolean emulatorStarted = Boolean.TRUE.equals(session.getAttribute("emulatorStarted"));
|
boolean emulatorStarted = Boolean.TRUE.equals(session.getAttribute("emulatorStarted"));
|
||||||
@@ -286,7 +295,25 @@ public final class MainView extends Main implements BeforeEnterObserver {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
|
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() {
|
private void startup() {
|
||||||
@@ -310,6 +337,8 @@ public final class MainView extends Main implements BeforeEnterObserver {
|
|||||||
downloadApp();
|
downloadApp();
|
||||||
|
|
||||||
installApp();
|
installApp();
|
||||||
|
|
||||||
|
startApp();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
// Logging, Fehlerbehandlung …
|
// 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() {
|
private void installApp() {
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
|
||||||
@@ -609,6 +715,7 @@ public final class MainView extends Main implements BeforeEnterObserver {
|
|||||||
logger.error("Fehler beim HTTP-Request", e);
|
logger.error("Fehler beim HTTP-Request", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildContainerName(NiederlassungInfo info) {
|
private String buildContainerName(NiederlassungInfo info) {
|
||||||
return "android-container-" + info.name();
|
return "android-container-" + info.name();
|
||||||
}
|
}
|
||||||
@@ -618,14 +725,12 @@ public final class MainView extends Main implements BeforeEnterObserver {
|
|||||||
logger.warn("isContainerStarted: Niederlassung ist null");
|
logger.warn("isContainerStarted: Niederlassung ist null");
|
||||||
return false;
|
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);
|
String name = buildContainerName(info);
|
||||||
try {
|
try {
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create("http://" + srv + ":2375/containers/" + name + "/json"))
|
.uri(URI.create("http://" + srv + ":2375/containers/" + name + "/json")).GET().build();
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
int status = response.statusCode();
|
int status = response.statusCode();
|
||||||
logger.info("Inspect-Container '{}': status={} ", name, status);
|
logger.info("Inspect-Container '{}': status={} ", name, status);
|
||||||
@@ -645,7 +750,6 @@ public final class MainView extends Main implements BeforeEnterObserver {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Dialog showWaitDialog(String message) {
|
private Dialog showWaitDialog(String message) {
|
||||||
Dialog dialog = new Dialog();
|
Dialog dialog = new Dialog();
|
||||||
dialog.setModal(true);
|
dialog.setModal(true);
|
||||||
@@ -735,45 +839,12 @@ public final class MainView extends Main implements BeforeEnterObserver {
|
|||||||
logger.info("Starte Logout-Cleanup nach Shutdown");
|
logger.info("Starte Logout-Cleanup nach Shutdown");
|
||||||
dialog.close();
|
dialog.close();
|
||||||
|
|
||||||
// Session cleanup erst nach Shutdown
|
loginSessionService.cleanupApplicationSession(ui.getSession());
|
||||||
String sessionId = (String) ui.getSession().getAttribute("sessionId");
|
if (ssoConfiguration.isEnabled()) {
|
||||||
if (sessionId != null) {
|
authenticationContext.logout();
|
||||||
Application.activeSessions.remove(sessionId);
|
} else {
|
||||||
logger.info("Session {} aus aktiven Sessions entfernt", sessionId);
|
ui.navigate(SimCardConfigurationView.class);
|
||||||
Application.sessionsById.remove(sessionId);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
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) {
|
} catch (Exception ex) {
|
||||||
logger.error("Fehler beim Logout UI-Update", 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.BeforeEnterEvent;
|
||||||
import com.vaadin.flow.router.BeforeEnterObserver;
|
import com.vaadin.flow.router.BeforeEnterObserver;
|
||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
|
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
||||||
|
import de.assecutor.emulatorstation.base.ui.view.security.SsoConfiguration;
|
||||||
|
|
||||||
@Route("")
|
@Route("")
|
||||||
|
@AnonymousAllowed
|
||||||
public class RootView implements BeforeEnterObserver {
|
public class RootView implements BeforeEnterObserver {
|
||||||
|
|
||||||
|
private final SsoConfiguration ssoConfiguration;
|
||||||
|
|
||||||
|
public RootView(SsoConfiguration ssoConfiguration) {
|
||||||
|
this.ssoConfiguration = ssoConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeEnter(BeforeEnterEvent event) {
|
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 com.vaadin.flow.spring.security.VaadinWebSecurity;
|
||||||
import de.assecutor.emulatorstation.base.ui.view.LoginView;
|
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.context.annotation.Configuration;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
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;
|
import de.assecutor.emulatorstation.Application;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class SecurityConfig extends VaadinWebSecurity {
|
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
|
@Override
|
||||||
protected void configure(HttpSecurity http) throws Exception {
|
protected void configure(HttpSecurity http) throws Exception {
|
||||||
super.configure(http);
|
super.configure(http);
|
||||||
|
|
||||||
|
if (ssoConfiguration.isEnabled()) {
|
||||||
setLoginView(http, LoginView.class);
|
setLoginView(http, LoginView.class);
|
||||||
|
} else {
|
||||||
|
setLoginView(http, SimCardConfigurationView.class);
|
||||||
|
}
|
||||||
|
|
||||||
http.sessionManagement(session -> session
|
http.sessionManagement(session -> session
|
||||||
// Wichtig: Session-Fixation so konfigurieren, dass Session-Attribute (z.B. "user") erhalten bleiben
|
// Wichtig: Session-Fixation so konfigurieren, dass Session-Attribute (z.B. "user") erhalten bleiben
|
||||||
.sessionFixation(fixation -> fixation.migrateSession())
|
.sessionFixation(fixation -> fixation.migrateSession()).maximumSessions(Application.MAX_ACTIVE_SESSIONS)
|
||||||
.maximumSessions(Application.MAX_ACTIVE_SESSIONS)
|
.maxSessionsPreventsLogin(false));
|
||||||
.maxSessionsPreventsLogin(false))
|
|
||||||
.logout(
|
if (ssoConfiguration.isEnabled()) {
|
||||||
logout -> logout.logoutSuccessUrl("/login").addLogoutHandler((request, response, authentication) -> {
|
LogoutSuccessHandler logoutSuccessHandler = oidcLogoutSuccessHandler("{baseUrl}/login");
|
||||||
if (authentication != null) {
|
|
||||||
String username = authentication.getName();
|
http.oauth2Login(
|
||||||
Application.activeNiederlassungen.entrySet()
|
oauth2 -> oauth2.loginPage("/login").defaultSuccessUrl("/login", true).failureUrl("/login?error"));
|
||||||
.removeIf(entry -> entry.getValue().equals(username));
|
|
||||||
|
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
|
@Override
|
||||||
public void sessionDestroy(SessionDestroyEvent event) {
|
public void sessionDestroy(SessionDestroyEvent event) {
|
||||||
String username = (String) event.getSession().getAttribute("user");
|
|
||||||
NiederlassungInfo niederlassung = (NiederlassungInfo) event.getSession().getAttribute("niederlassung");
|
NiederlassungInfo niederlassung = (NiederlassungInfo) event.getSession().getAttribute("niederlassung");
|
||||||
String sessionId = (String) event.getSession().getAttribute("sessionId");
|
String sessionId = (String) event.getSession().getAttribute("sessionId");
|
||||||
|
|
||||||
if (username != null && niederlassung != null) {
|
if (niederlassung != null) {
|
||||||
Application.activeNiederlassungen.remove(niederlassung.name());
|
Application.activeNiederlassungen.remove(niederlassung.name());
|
||||||
}
|
}
|
||||||
if (sessionId != null) {
|
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}
|
server.port=${PORT:8080}
|
||||||
logging.level.org.atmosphere=warn
|
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
|
# Launch the default browser when starting the application in development mode
|
||||||
vaadin.launch-browser=true
|
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.http-only=true
|
||||||
server.servlet.session.cookie.secure=false
|
server.servlet.session.cookie.secure=false
|
||||||
server.servlet.session.persistent=true
|
server.servlet.session.persistent=true
|
||||||
|
server.forward-headers-strategy=framework
|
||||||
|
|
||||||
# Vaadin session configuration
|
# Vaadin session configuration
|
||||||
vaadin.heartbeatInterval=300
|
vaadin.heartbeatInterval=300
|
||||||
vaadin.closeIdleSessions=false
|
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
|
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