Compare commits
13 Commits
fdd319a461
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b313cd588 | |||
| 9a67832faa | |||
| 4fc5b04a68 | |||
| f764b4a7aa | |||
| 297d0cf000 | |||
| 0e28a388b7 | |||
| 5fbf05b420 | |||
| f3f8f90737 | |||
| 479eb5a65a | |||
| ddfb8a692b | |||
| 0747d131ee | |||
| 562e9bf16e | |||
| 5b0be56914 |
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}"
|
||||||
9513
package-lock.json
generated
Normal file
9513
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
110
package.json
Normal file
110
package.json
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"name": "no-name",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@polymer/polymer": "3.5.2",
|
||||||
|
"@vaadin/bundles": "24.7.7",
|
||||||
|
"@vaadin/common-frontend": "0.0.19",
|
||||||
|
"@vaadin/polymer-legacy-adapter": "24.7.7",
|
||||||
|
"@vaadin/react-components": "24.7.7",
|
||||||
|
"@vaadin/react-components-pro": "24.7.7",
|
||||||
|
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||||
|
"@vaadin/vaadin-lumo-styles": "24.7.7",
|
||||||
|
"@vaadin/vaadin-material-styles": "24.7.7",
|
||||||
|
"@vaadin/vaadin-themable-mixin": "24.7.7",
|
||||||
|
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||||
|
"construct-style-sheets-polyfill": "3.1.0",
|
||||||
|
"date-fns": "2.29.3",
|
||||||
|
"lit": "3.3.0",
|
||||||
|
"proj4": "2.15.0",
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-dom": "18.3.1",
|
||||||
|
"react-router": "7.5.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/preset-react": "7.26.3",
|
||||||
|
"@preact/signals-react-transform": "0.5.1",
|
||||||
|
"@rollup/plugin-replace": "6.0.2",
|
||||||
|
"@rollup/pluginutils": "5.1.4",
|
||||||
|
"@types/react": "18.3.20",
|
||||||
|
"@types/react-dom": "18.3.6",
|
||||||
|
"@vitejs/plugin-react": "4.4.1",
|
||||||
|
"async": "3.2.6",
|
||||||
|
"glob": "11.0.2",
|
||||||
|
"rollup-plugin-brotli": "3.1.0",
|
||||||
|
"rollup-plugin-visualizer": "5.14.0",
|
||||||
|
"strip-css-comments": "5.0.0",
|
||||||
|
"transform-ast": "2.4.4",
|
||||||
|
"typescript": "5.7.3",
|
||||||
|
"vite": "6.3.4",
|
||||||
|
"vite-plugin-checker": "0.9.1",
|
||||||
|
"workbox-build": "7.3.0",
|
||||||
|
"workbox-core": "7.3.0",
|
||||||
|
"workbox-precaching": "7.3.0"
|
||||||
|
},
|
||||||
|
"vaadin": {
|
||||||
|
"dependencies": {
|
||||||
|
"@polymer/polymer": "3.5.2",
|
||||||
|
"@vaadin/bundles": "24.7.7",
|
||||||
|
"@vaadin/common-frontend": "0.0.19",
|
||||||
|
"@vaadin/polymer-legacy-adapter": "24.7.7",
|
||||||
|
"@vaadin/react-components": "24.7.7",
|
||||||
|
"@vaadin/react-components-pro": "24.7.7",
|
||||||
|
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||||
|
"@vaadin/vaadin-lumo-styles": "24.7.7",
|
||||||
|
"@vaadin/vaadin-material-styles": "24.7.7",
|
||||||
|
"@vaadin/vaadin-themable-mixin": "24.7.7",
|
||||||
|
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||||
|
"construct-style-sheets-polyfill": "3.1.0",
|
||||||
|
"date-fns": "2.29.3",
|
||||||
|
"lit": "3.3.0",
|
||||||
|
"proj4": "2.15.0",
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-dom": "18.3.1",
|
||||||
|
"react-router": "7.5.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/preset-react": "7.26.3",
|
||||||
|
"@preact/signals-react-transform": "0.5.1",
|
||||||
|
"@rollup/plugin-replace": "6.0.2",
|
||||||
|
"@rollup/pluginutils": "5.1.4",
|
||||||
|
"@types/react": "18.3.20",
|
||||||
|
"@types/react-dom": "18.3.6",
|
||||||
|
"@vitejs/plugin-react": "4.4.1",
|
||||||
|
"async": "3.2.6",
|
||||||
|
"glob": "11.0.2",
|
||||||
|
"rollup-plugin-brotli": "3.1.0",
|
||||||
|
"rollup-plugin-visualizer": "5.14.0",
|
||||||
|
"strip-css-comments": "5.0.0",
|
||||||
|
"transform-ast": "2.4.4",
|
||||||
|
"typescript": "5.7.3",
|
||||||
|
"vite": "6.3.4",
|
||||||
|
"vite-plugin-checker": "0.9.1",
|
||||||
|
"workbox-build": "7.3.0",
|
||||||
|
"workbox-core": "7.3.0",
|
||||||
|
"workbox-precaching": "7.3.0"
|
||||||
|
},
|
||||||
|
"hash": "c18408e57a0875a842d7f93da064e1b59d0f12787cf31c11320e67bdbc80cdb5"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@vaadin/bundles": "$@vaadin/bundles",
|
||||||
|
"@vaadin/polymer-legacy-adapter": "$@vaadin/polymer-legacy-adapter",
|
||||||
|
"@vaadin/vaadin-development-mode-detector": "$@vaadin/vaadin-development-mode-detector",
|
||||||
|
"@vaadin/vaadin-usage-statistics": "$@vaadin/vaadin-usage-statistics",
|
||||||
|
"@vaadin/react-components": "$@vaadin/react-components",
|
||||||
|
"@vaadin/react-components-pro": "$@vaadin/react-components-pro",
|
||||||
|
"@vaadin/common-frontend": "$@vaadin/common-frontend",
|
||||||
|
"react-dom": "$react-dom",
|
||||||
|
"construct-style-sheets-polyfill": "$construct-style-sheets-polyfill",
|
||||||
|
"lit": "$lit",
|
||||||
|
"@polymer/polymer": "$@polymer/polymer",
|
||||||
|
"react": "$react",
|
||||||
|
"react-router": "$react-router",
|
||||||
|
"date-fns": "$date-fns",
|
||||||
|
"proj4": "$proj4",
|
||||||
|
"@vaadin/vaadin-themable-mixin": "$@vaadin/vaadin-themable-mixin",
|
||||||
|
"@vaadin/vaadin-lumo-styles": "$@vaadin/vaadin-lumo-styles",
|
||||||
|
"@vaadin/vaadin-material-styles": "$@vaadin/vaadin-material-styles"
|
||||||
|
}
|
||||||
|
}
|
||||||
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.7</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,24 +3,21 @@ 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;
|
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@Push
|
@Push
|
||||||
|
@EnableScheduling
|
||||||
|
|
||||||
@Theme("default")
|
@Theme("default")
|
||||||
public class Application implements AppShellConfigurator {
|
public class Application implements AppShellConfigurator {
|
||||||
public static final Map<String, UserInfo> users = Map.ofEntries(
|
public static final int MAX_ACTIVE_SESSIONS = 5;
|
||||||
Map.entry("user1", new UserInfo("pass1")),
|
|
||||||
Map.entry("user2", new UserInfo("pass2")),
|
|
||||||
Map.entry("user3", new UserInfo("pass3")),
|
|
||||||
Map.entry("user4", new UserInfo("pass4")),
|
|
||||||
Map.entry("user5", new UserInfo("pass5"))
|
|
||||||
);
|
|
||||||
|
|
||||||
public static final Map<String, NiederlassungInfo> niederlassungen = Map.ofEntries(
|
public static final Map<String, NiederlassungInfo> niederlassungen = Map.ofEntries(
|
||||||
Map.entry("Berlin", new NiederlassungInfo("Berlin", "172.18.0.103", "6083", "/berlin")),
|
Map.entry("Berlin", new NiederlassungInfo("Berlin", "172.18.0.103", "6083", "/berlin")),
|
||||||
@@ -31,13 +28,13 @@ 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",
|
||||||
Map.entry("Geschäftführung", new NiederlassungInfo("Geschäftführung", "172.18.0.112", "6092", "/gfl")),
|
new NiederlassungInfo("Frankfurt am Main", "172.18.0.105", "6085", "/frankfurt")),
|
||||||
Map.entry("Admin", new NiederlassungInfo("Admin", "172.18.0.110", "6090", "/admin"))
|
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 Map<String, String> activeNiederlassungen = new ConcurrentHashMap<>();
|
public static final Map<String, String> activeNiederlassungen = new ConcurrentHashMap<>();
|
||||||
public static final Map<String, String> activeUsers = new ConcurrentHashMap<>();
|
public static final Map<String, String> activeSessions = new ConcurrentHashMap<>(); // sessionId -> niederlassung
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(Application.class, args);
|
SpringApplication.run(Application.class, args);
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package de.assecutor.emulatorstation.base.domain;
|
||||||
|
|
||||||
|
import de.assecutor.emulatorstation.Application;
|
||||||
|
import de.assecutor.emulatorstation.base.ui.view.security.EmulatorServerConfiguration;
|
||||||
|
import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ContainerShutdownScheduler {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ContainerShutdownScheduler.class);
|
||||||
|
|
||||||
|
private final HttpClient http = HttpClient.newHttpClient();
|
||||||
|
private final EmulatorServerConfiguration emulatorServerConfiguration;
|
||||||
|
|
||||||
|
public ContainerShutdownScheduler(EmulatorServerConfiguration emulatorServerConfiguration) {
|
||||||
|
this.emulatorServerConfiguration = emulatorServerConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Täglich um 22:00 Uhr Serverzeit
|
||||||
|
@Scheduled(cron = "0 0 22 * * *")
|
||||||
|
public void shutdownAllRunningContainers() {
|
||||||
|
logger.info("[Scheduler] Starte nächtliches Herunterfahren aller laufenden Emulator-Container (22:00)");
|
||||||
|
for (NiederlassungInfo info : Application.niederlassungen.values()) {
|
||||||
|
String containerName = buildContainerName(info);
|
||||||
|
try {
|
||||||
|
if (isRunning(containerName)) {
|
||||||
|
stopContainer(containerName);
|
||||||
|
} else {
|
||||||
|
logger.info("[Scheduler] Container '{}' läuft nicht – nichts zu tun", containerName);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("[Scheduler] Fehler beim Beenden des Containers '{}': {}", containerName, e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info("[Scheduler] Nächtliches Herunterfahren abgeschlossen");
|
||||||
|
|
||||||
|
// Alle Sessions beenden (Logout erzwingen)
|
||||||
|
logger.info("[Scheduler] Beginne Session-Logout für alle aktiven Sessions: {}",
|
||||||
|
Application.sessionsById.size());
|
||||||
|
for (com.vaadin.flow.server.WrappedSession session : new java.util.ArrayList<>(
|
||||||
|
Application.sessionsById.values())) {
|
||||||
|
try {
|
||||||
|
session.invalidate();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("[Scheduler] Fehler beim Invalidieren einer Session: {}", e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Zur Sicherheit interne Mappings leeren
|
||||||
|
Application.activeSessions.clear();
|
||||||
|
Application.activeNiederlassungen.clear();
|
||||||
|
Application.sessionsById.clear();
|
||||||
|
logger.info("[Scheduler] Session-Logout abgeschlossen. Aktive Sessions: {} / Maps geleert.",
|
||||||
|
Application.activeSessions.size());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildContainerName(NiederlassungInfo info) {
|
||||||
|
return "android-container-" + info.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRunning(String containerName) throws Exception {
|
||||||
|
String dockerHost = emulatorServerConfiguration.getServerIp();
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create("http://" + dockerHost + ":2375/containers/" + containerName + "/json")).GET().build();
|
||||||
|
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (resp.statusCode() == 200) {
|
||||||
|
String body = resp.body();
|
||||||
|
boolean running = body != null && body.contains("\"Running\":true");
|
||||||
|
logger.info("[Scheduler] Inspect '{}' -> running={}", containerName, running);
|
||||||
|
return running;
|
||||||
|
} else if (resp.statusCode() == 404) {
|
||||||
|
logger.info("[Scheduler] Container '{}' existiert nicht (404)", containerName);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
logger.info("[Scheduler] Inspect '{}' -> status {}", containerName, resp.statusCode());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopContainer(String containerName) throws Exception {
|
||||||
|
String dockerHost = emulatorServerConfiguration.getServerIp();
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create("http://" + dockerHost + ":2375/containers/" + containerName + "/stop"))
|
||||||
|
.POST(HttpRequest.BodyPublishers.noBody()).build();
|
||||||
|
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
int status = resp.statusCode();
|
||||||
|
if (status == 204 || status == 304) {
|
||||||
|
logger.info("[Scheduler] Container '{}' gestoppt (status={})", containerName, status);
|
||||||
|
} else if (status == 404) {
|
||||||
|
logger.info("[Scheduler] Container '{}' nicht gefunden (404)", containerName);
|
||||||
|
} else {
|
||||||
|
logger.warn("[Scheduler] Unerwarteter Status beim Stoppen von '{}' -> {} / {}", containerName, status,
|
||||||
|
resp.body());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,100 +1,108 @@
|
|||||||
package de.assecutor.emulatorstation.base.ui.view;
|
package de.assecutor.emulatorstation.base.ui.view;
|
||||||
|
|
||||||
import com.vaadin.flow.component.html.Span;
|
|
||||||
import com.vaadin.flow.component.login.LoginForm;
|
|
||||||
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.textfield.TextField;
|
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.html.H1;
|
||||||
|
import com.vaadin.flow.component.icon.Icon;
|
||||||
|
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||||
import com.vaadin.flow.component.notification.Notification;
|
import com.vaadin.flow.component.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;
|
|
||||||
|
|
||||||
@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() {
|
public LoginView(AuthenticationContext authenticationContext, LoginSessionService loginSessionService,
|
||||||
|
SsoConfiguration ssoConfiguration) {
|
||||||
|
this.authenticationContext = authenticationContext;
|
||||||
|
this.loginSessionService = loginSessionService;
|
||||||
|
this.ssoConfiguration = ssoConfiguration;
|
||||||
|
|
||||||
|
setSizeFull();
|
||||||
setAlignItems(Alignment.CENTER);
|
setAlignItems(Alignment.CENTER);
|
||||||
|
setJustifyContentMode(JustifyContentMode.CENTER);
|
||||||
|
getStyle().set("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)").set("min-height", "100vh")
|
||||||
|
.set("padding", "20px");
|
||||||
|
|
||||||
Span title = new Span("Anmelden");
|
Div loginContainer = new Div();
|
||||||
title.getStyle().set("font-size", "24px").set("font-weight", "bold");
|
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", "440px")
|
||||||
|
.set("width", "100%").set("text-align", "center");
|
||||||
|
|
||||||
TextField usernameField = new TextField("Benutzername");
|
Icon icon = new Icon(VaadinIcon.DESKTOP);
|
||||||
PasswordField passwordField = new PasswordField("Passwort");
|
icon.setSize("64px");
|
||||||
|
icon.getStyle().set("color", "#667eea").set("margin-bottom", "20px");
|
||||||
|
|
||||||
Select<String> niederlassungSelect = new Select<>();
|
H1 title = new H1("Emulator Station");
|
||||||
niederlassungSelect.setLabel("Niederlassung");
|
title.getStyle().set("margin", "0 0 8px 0").set("color", "#333333").set("font-size", "2rem").set("font-weight",
|
||||||
niederlassungSelect.setItems(Application.niederlassungen.keySet().stream().sorted().toList());
|
"600");
|
||||||
niederlassungSelect.setPlaceholder("Wählen Sie eine Niederlassung");
|
|
||||||
|
|
||||||
Button loginButton = new Button("Anmelden", event -> {
|
VerticalLayout formLayout = new VerticalLayout();
|
||||||
String username = usernameField.getValue();
|
formLayout.setSpacing(true);
|
||||||
String password = passwordField.getValue();
|
formLayout.setPadding(false);
|
||||||
String niederlassung = niederlassungSelect.getValue();
|
formLayout.setWidthFull();
|
||||||
|
|
||||||
if (username.isEmpty() || password.isEmpty() || niederlassung == null) {
|
Button loginButton = new Button("Mit Microsoft anmelden", new Icon(VaadinIcon.SIGN_IN), event -> startLogin());
|
||||||
Notification.show("Bitte alle Felder ausfüllen", 3000, Notification.Position.MIDDLE);
|
loginButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE);
|
||||||
return;
|
loginButton.setWidthFull();
|
||||||
}
|
loginButton.getStyle().set("margin-top", "8px")
|
||||||
|
.set("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)").set("border", "none")
|
||||||
|
.set("font-weight", "600");
|
||||||
|
|
||||||
if (!Application.users.containsKey(username) ||
|
formLayout.add(loginButton);
|
||||||
!Application.users.get(username).password().equals(password)) {
|
loginContainer.add(icon, title, formLayout);
|
||||||
Notification.show("Ungültige Anmeldedaten", 3000, Notification.Position.MIDDLE);
|
add(loginContainer);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Application.activeUsers.containsKey(username)) {
|
|
||||||
Notification.show("Benutzer ist bereits angemeldet", 3000, Notification.Position.MIDDLE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Application.activeNiederlassungen.containsKey(niederlassung)) {
|
|
||||||
Notification.show("Niederlassung ist bereits von einem anderen Benutzer belegt", 3000, Notification.Position.MIDDLE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Application.activeNiederlassungen.put(niederlassung, username);
|
|
||||||
Application.activeUsers.put(username, niederlassung);
|
|
||||||
|
|
||||||
// Spring Security Authentifizierung setzen
|
|
||||||
var authorities = List.of(new SimpleGrantedAuthority("ROLE_USER"));
|
|
||||||
var authentication = new UsernamePasswordAuthenticationToken(username, password, authorities);
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
|
||||||
|
|
||||||
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 -> {
|
|
||||||
ui.getSession().setAttribute("user", username);
|
|
||||||
ui.getSession().setAttribute("username", username);
|
|
||||||
ui.getSession().setAttribute("niederlassung", niederlassungInfo);
|
|
||||||
|
|
||||||
logger.info("Login erfolgreich - Session-Daten gesetzt:");
|
|
||||||
logger.info("Username: {}", username);
|
|
||||||
logger.info("Niederlassung: {}", niederlassungInfo.name());
|
|
||||||
|
|
||||||
ui.navigate("main");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
add(title, usernameField, passwordField, niederlassungSelect, loginButton);
|
|
||||||
|
|
||||||
if (MainLayout.instance != null) {
|
if (MainLayout.instance != null) {
|
||||||
MainLayout.instance.setDrawerOpened(false);
|
MainLayout.instance.setDrawerOpened(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterNavigation(AfterNavigationEvent event) {
|
||||||
|
getUI().ifPresent(ui -> {
|
||||||
|
if (!ssoConfiguration.isEnabled()) {
|
||||||
|
ui.navigate(SimCardConfigurationView.class);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authenticationContext.isAuthenticated()) {
|
||||||
|
if (loginSessionService.hasApplicationSession(ui.getSession())
|
||||||
|
&& loginSessionService.hasCompletedSimConfiguration(ui.getSession())) {
|
||||||
|
ui.navigate(MainView.class);
|
||||||
|
} else {
|
||||||
|
ui.navigate(SimCardConfigurationView.class);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.getLocation().getQueryParameters().getParameters().containsKey("error")) {
|
||||||
|
Notification.show("Die Microsoft-Anmeldung konnte nicht abgeschlossen werden.", 5000,
|
||||||
|
Notification.Position.MIDDLE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void 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;
|
||||||
|
|
||||||
@@ -42,26 +42,18 @@ public final class MainLayout extends AppLayout {
|
|||||||
UI ui = UI.getCurrent();
|
UI ui = UI.getCurrent();
|
||||||
ui.setPollInterval(60000); // Poll-Intervall auf 60 Sekunden setzen
|
ui.setPollInterval(60000); // Poll-Intervall auf 60 Sekunden setzen
|
||||||
|
|
||||||
// Session expiration handling
|
// Session keep-alive - no automatic invalidation on reload
|
||||||
ui.addPollListener(event -> {
|
ui.addPollListener(event -> {
|
||||||
if (!isUserSessionValid()) {
|
// Just keep the session alive, don't invalidate
|
||||||
VaadinSession.getCurrent().getSession().invalidate(); // Session invalidieren
|
// Session timeout is now handled by Spring Security configuration
|
||||||
ui.access(() -> ui.navigate("main")); // Immer zur Main-Seite weiterleiten
|
if (VaadinSession.getCurrent() != null) {
|
||||||
|
// Touch the session to keep it alive
|
||||||
|
VaadinSession.getCurrent().getSession().getMaxInactiveInterval();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isUserSessionValid() {
|
|
||||||
// Logik für Sitzungsprüfung, z.B. Timeout-Zeit überprüfen
|
|
||||||
return VaadinSession.getCurrent() != null
|
|
||||||
&& VaadinSession.getCurrent().getSession() != null
|
|
||||||
&& VaadinSession.getCurrent().getSession().getLastAccessedTime() + 300000
|
|
||||||
> System.currentTimeMillis(); // 300000 ms = 5 Minuten
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
@@ -89,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);
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import com.vaadin.flow.component.button.ButtonVariant;
|
|||||||
import com.vaadin.flow.component.dialog.Dialog;
|
import com.vaadin.flow.component.dialog.Dialog;
|
||||||
import com.vaadin.flow.component.html.Div;
|
import com.vaadin.flow.component.html.Div;
|
||||||
import com.vaadin.flow.component.html.H2;
|
import com.vaadin.flow.component.html.H2;
|
||||||
|
import com.vaadin.flow.component.html.H3;
|
||||||
import com.vaadin.flow.component.html.IFrame;
|
import com.vaadin.flow.component.html.IFrame;
|
||||||
import com.vaadin.flow.component.html.Paragraph;
|
import com.vaadin.flow.component.html.Paragraph;
|
||||||
|
import com.vaadin.flow.component.progressbar.ProgressBar;
|
||||||
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.orderedlayout.HorizontalLayout;
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
@@ -18,11 +20,17 @@ import com.vaadin.flow.router.BeforeEnterObserver;
|
|||||||
import com.vaadin.flow.component.UI;
|
import com.vaadin.flow.component.UI;
|
||||||
import com.vaadin.flow.component.html.Main;
|
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.server.VaadinSession;
|
||||||
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
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;
|
||||||
|
|
||||||
@@ -32,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
|
|
||||||
@Route("main")
|
|
||||||
@AnonymousAllowed
|
@AnonymousAllowed
|
||||||
public final class MainView extends Main implements BeforeEnterObserver
|
@Route("main")
|
||||||
{
|
@PreserveOnRefresh
|
||||||
|
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;
|
||||||
@@ -50,71 +63,109 @@ 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()
|
getStyle().set("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)").set("min-height", "100vh")
|
||||||
.set("display", "flex")
|
.set("display", "flex").set("flex-direction", "column").set("align-items", "center")
|
||||||
.set("flex-direction", "column")
|
.set("justify-content", "center").set("padding", "2.5%").set("box-sizing", "border-box")
|
||||||
.set("align-items", "center")
|
|
||||||
.set("justify-content", "center")
|
|
||||||
.set("padding", "5%")
|
|
||||||
.set("box-sizing", "border-box")
|
|
||||||
.set("overflow", "hidden");
|
.set("overflow", "hidden");
|
||||||
|
|
||||||
var contentContainer = new Div();
|
var contentContainer = new Div();
|
||||||
contentContainer.getStyle()
|
contentContainer.getStyle().set("width", "95%").set("height", "95%").set("display", "flex")
|
||||||
.set("width", "95%")
|
.set("flex-direction", "column").set("box-sizing", "border-box");
|
||||||
.set("height", "95%")
|
|
||||||
.set("display", "flex")
|
|
||||||
.set("flex-direction", "column")
|
|
||||||
.set("box-sizing", "border-box");
|
|
||||||
|
|
||||||
setupInactivityTimer();
|
|
||||||
setupWelcomeMessage(contentContainer);
|
setupWelcomeMessage(contentContainer);
|
||||||
setupEmulatorContainer(contentContainer);
|
setupEmulatorContainer(contentContainer);
|
||||||
setupButtonLayout(contentContainer);
|
setupButtonLayout(contentContainer);
|
||||||
|
|
||||||
add(contentContainer);
|
add(contentContainer);
|
||||||
addAttachListener(event -> loadSessionData());
|
addAttachListener(event -> {
|
||||||
|
loadSessionData();
|
||||||
|
setupSessionCleanupListener();
|
||||||
|
restoreUIStateIfNeeded();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kein Cleanup mehr beim Detach, um Reload-Resume zu ermöglichen
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupInactivityTimer() {
|
private void setupSessionCleanupListener() {
|
||||||
String jsCode = """
|
VaadinSession session = VaadinSession.getCurrent();
|
||||||
var inactivityTime = function () {
|
if (session != null) {
|
||||||
var time;
|
Boolean registered = (Boolean) session.getAttribute("cleanupListenerRegistered");
|
||||||
window.onload = resetTimer;
|
if (Boolean.TRUE.equals(registered)) {
|
||||||
document.onmousemove = resetTimer;
|
return;
|
||||||
document.onkeypress = resetTimer;
|
}
|
||||||
document.onclick = resetTimer;
|
session.addSessionDestroyListener(event -> {
|
||||||
document.onscroll = resetTimer;
|
logger.info("Session destroy event triggered - cleaning up resources");
|
||||||
|
// Cleanup will be done in a background thread to avoid blocking
|
||||||
function logout() {
|
executor.submit(() -> {
|
||||||
$0.$server.logout();
|
try {
|
||||||
|
cleanupResources();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.error("Error during session cleanup", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
session.setAttribute("cleanupListenerRegistered", true);
|
||||||
|
logger.info("Session cleanup listener registered for session: {}", session.getSession().getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetTimer() {
|
private void cleanupResources() {
|
||||||
clearTimeout(time);
|
try {
|
||||||
time = setTimeout(logout, 4 * 60 * 60 * 1000);
|
logger.info("Starting resource cleanup");
|
||||||
}
|
|
||||||
};
|
|
||||||
inactivityTime();
|
|
||||||
""";
|
|
||||||
|
|
||||||
UI.getCurrent().getPage().executeJs(jsCode, this);
|
// Get session information before cleanup
|
||||||
|
String sessionId = null;
|
||||||
|
String niederlassungName = null;
|
||||||
|
|
||||||
|
if (getUI().isPresent()) {
|
||||||
|
VaadinSession session = getUI().get().getSession();
|
||||||
|
if (session != null) {
|
||||||
|
sessionId = (String) session.getAttribute("sessionId");
|
||||||
|
if (niederlassung != null) {
|
||||||
|
niederlassungName = niederlassung.name();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown containers
|
||||||
|
logger.info("Shutting down containers for niederlassung: {}", niederlassungName);
|
||||||
|
shutdown();
|
||||||
|
|
||||||
|
// Remove from active sessions and niederlassungen
|
||||||
|
if (sessionId != null) {
|
||||||
|
Application.activeSessions.remove(sessionId);
|
||||||
|
logger.info("Session {} removed from active sessions", sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (niederlassungName != null) {
|
||||||
|
Application.activeNiederlassungen.remove(niederlassungName);
|
||||||
|
logger.info("Niederlassung '{}' wurde freigegeben und ist wieder verfügbar für neue Anmeldungen",
|
||||||
|
niederlassungName);
|
||||||
|
logger.info("Aktive Niederlassungen nach Freigabe: {}", Application.activeNiederlassungen.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Resource cleanup completed successfully");
|
||||||
|
logger.info("Active sessions after cleanup: {}/{}", Application.activeSessions.size(),
|
||||||
|
Application.MAX_ACTIVE_SESSIONS);
|
||||||
|
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.error("Error during resource cleanup", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupWelcomeMessage(Div container) {
|
private void setupWelcomeMessage(Div container) {
|
||||||
welcomeMessage.addClassName("welcome-panel");
|
welcomeMessage.addClassName("welcome-panel");
|
||||||
welcomeMessage.getStyle()
|
welcomeMessage.getStyle().set("display", "flex").set("flex-direction", "column").set("align-items", "center")
|
||||||
.set("display", "flex")
|
.set("justify-content", "center").set("flex-grow", "1").set("background-color", "#f8f9fa")
|
||||||
.set("flex-direction", "column")
|
.set("border", "2px dashed #dee2e6").set("border-radius", "8px").set("color", "#6c757d")
|
||||||
.set("align-items", "center")
|
|
||||||
.set("justify-content", "center")
|
|
||||||
.set("flex-grow", "1")
|
|
||||||
.set("background-color", "#f8f9fa")
|
|
||||||
.set("border", "2px dashed #dee2e6")
|
|
||||||
.set("border-radius", "8px")
|
|
||||||
.set("color", "#6c757d")
|
|
||||||
.set("margin-bottom", "16px");
|
.set("margin-bottom", "16px");
|
||||||
|
|
||||||
Icon icon = new Icon(VaadinIcon.DESKTOP);
|
Icon icon = new Icon(VaadinIcon.DESKTOP);
|
||||||
@@ -124,7 +175,8 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
H2 title = new H2("Willkommen bei der Emulator Station");
|
H2 title = new H2("Willkommen bei der Emulator Station");
|
||||||
title.getStyle().set("margin", "0 0 6px 0").set("color", "#495057").set("font-size", "1.5rem");
|
title.getStyle().set("margin", "0 0 6px 0").set("color", "#495057").set("font-size", "1.5rem");
|
||||||
|
|
||||||
Paragraph description = new Paragraph("Klicken Sie auf 'Start', um Ihren Android Emulator zu starten.");
|
Paragraph description = new Paragraph(
|
||||||
|
"Klicken Sie auf 'Emulator starten', um Ihren Android Emulator zu starten.");
|
||||||
description.getStyle().set("margin", "0").set("text-align", "center").set("font-size", "0.9rem");
|
description.getStyle().set("margin", "0").set("text-align", "center").set("font-size", "0.9rem");
|
||||||
|
|
||||||
welcomeMessage.add(icon, title, description);
|
welcomeMessage.add(icon, title, description);
|
||||||
@@ -134,32 +186,24 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
private void setupButtonLayout(Div container) {
|
private void setupButtonLayout(Div container) {
|
||||||
var buttonLayout = new HorizontalLayout();
|
var buttonLayout = new HorizontalLayout();
|
||||||
buttonLayout.setJustifyContentMode(HorizontalLayout.JustifyContentMode.CENTER);
|
buttonLayout.setJustifyContentMode(HorizontalLayout.JustifyContentMode.CENTER);
|
||||||
buttonLayout.getStyle()
|
buttonLayout.getStyle().set("margin", "0").set("padding", "8px 0").set("flex-shrink", "0").set("order", "999");
|
||||||
.set("margin", "0")
|
|
||||||
.set("padding", "8px 0")
|
|
||||||
.set("flex-shrink", "0")
|
|
||||||
.set("order", "999");
|
|
||||||
|
|
||||||
startBtn = new Button("Emulator starten", new Icon(VaadinIcon.PLAY), event -> startup());
|
startBtn = new Button("Emulator starten", new Icon(VaadinIcon.PLAY), event -> startup());
|
||||||
startBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
startBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
|
||||||
Button logoutBtn = new Button("Abmelden", new Icon(VaadinIcon.SIGN_OUT), event -> logout());
|
Button logoutBtn = new Button("Abmelden", new Icon(VaadinIcon.SIGN_OUT), event -> logout());
|
||||||
logoutBtn.addThemeVariants(ButtonVariant.LUMO_CONTRAST);
|
logoutBtn.addThemeVariants(ButtonVariant.LUMO_CONTRAST);
|
||||||
|
logoutBtn.getStyle().set("background-color", "#f5f5f5").set("color", "#333333").set("border",
|
||||||
|
"1px solid #d0d0d0");
|
||||||
|
|
||||||
buttonLayout.add(startBtn, logoutBtn);
|
buttonLayout.add(startBtn, logoutBtn);
|
||||||
container.add(buttonLayout);
|
container.add(buttonLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupEmulatorContainer(Div container) {
|
private void setupEmulatorContainer(Div container) {
|
||||||
emulatorContainer.getStyle()
|
emulatorContainer.getStyle().set("border", "2px solid #e9ecef").set("border-radius", "8px")
|
||||||
.set("border", "2px solid #e9ecef")
|
.set("background-color", "#ffffff").set("box-shadow", "0 2px 4px rgba(0,0,0,0.1)")
|
||||||
.set("border-radius", "8px")
|
.set("overflow", "hidden").set("display", "none").set("flex-grow", "1").set("margin-bottom", "16px");
|
||||||
.set("background-color", "#ffffff")
|
|
||||||
.set("box-shadow", "0 2px 4px rgba(0,0,0,0.1)")
|
|
||||||
.set("overflow", "hidden")
|
|
||||||
.set("display", "none")
|
|
||||||
.set("flex-grow", "1")
|
|
||||||
.set("margin-bottom", "16px");
|
|
||||||
|
|
||||||
webView.setSizeFull();
|
webView.setSizeFull();
|
||||||
ensureWebViewScrollbarsHidden();
|
ensureWebViewScrollbarsHidden();
|
||||||
@@ -172,26 +216,19 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
webView.addClassName("mainview-webview");
|
webView.addClassName("mainview-webview");
|
||||||
webView.getElement().setAttribute("frameborder", "0");
|
webView.getElement().setAttribute("frameborder", "0");
|
||||||
webView.getElement().setAttribute("scrolling", "no");
|
webView.getElement().setAttribute("scrolling", "no");
|
||||||
webView.getElement().getStyle()
|
webView.getElement().getStyle().set("border", "0").set("overflow", "hidden").set("-ms-overflow-style", "none")
|
||||||
.set("border", "0")
|
|
||||||
.set("overflow", "hidden")
|
|
||||||
.set("-ms-overflow-style", "none")
|
|
||||||
.set("scrollbar-width", "none");
|
.set("scrollbar-width", "none");
|
||||||
|
|
||||||
// Force-disable iframe scrollbars for all major engines
|
// Force-disable iframe scrollbars for all major engines
|
||||||
webView.getElement().executeJs(
|
webView.getElement().executeJs("const frame = this;"
|
||||||
"const frame = this;" +
|
+ "frame.style.setProperty('overflow', 'hidden', 'important');"
|
||||||
"frame.style.setProperty('overflow', 'hidden', 'important');" +
|
+ "frame.style.setProperty('scrollbar-width', 'none', 'important');"
|
||||||
"frame.style.setProperty('scrollbar-width', 'none', 'important');" +
|
+ "frame.style.setProperty('-ms-overflow-style', 'none', 'important');"
|
||||||
"frame.style.setProperty('-ms-overflow-style', 'none', 'important');" +
|
+ "frame.classList.add('mainview-webview');"
|
||||||
"frame.classList.add('mainview-webview');" +
|
+ "if (!document.getElementById('mainview-webview-scroll-style')) {"
|
||||||
"if (!document.getElementById('mainview-webview-scroll-style')) {" +
|
+ " const style = document.createElement('style');" + " style.id = 'mainview-webview-scroll-style';"
|
||||||
" const style = document.createElement('style');" +
|
+ " style.textContent = '.mainview-webview { overflow: hidden !important; scrollbar-width: none !important; -ms-overflow-style: none !important; } .mainview-webview::-webkit-scrollbar { display: none !important; width: 0 !important; height: 0 !important; }';"
|
||||||
" style.id = 'mainview-webview-scroll-style';" +
|
+ " document.head.appendChild(style);" + "}");
|
||||||
" style.textContent = '.mainview-webview { overflow: hidden !important; scrollbar-width: none !important; -ms-overflow-style: none !important; } .mainview-webview::-webkit-scrollbar { display: none !important; width: 0 !important; height: 0 !important; }';" +
|
|
||||||
" document.head.appendChild(style);" +
|
|
||||||
"}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadSessionData() {
|
private void loadSessionData() {
|
||||||
@@ -200,29 +237,83 @@ 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
|
||||||
|
if (niederlassung == null) {
|
||||||
|
try {
|
||||||
|
String sid = (String) vaadinSession.getAttribute("sessionId");
|
||||||
|
if (sid == null && vaadinSession.getSession() != null) {
|
||||||
|
sid = vaadinSession.getSession().getId();
|
||||||
|
}
|
||||||
|
if (sid != null) {
|
||||||
|
String nlName = Application.activeSessions.get(sid);
|
||||||
|
if (nlName != null) {
|
||||||
|
NiederlassungInfo info = Application.niederlassungen.get(nlName);
|
||||||
|
if (info != null) {
|
||||||
|
niederlassung = info;
|
||||||
|
vaadinSession.setAttribute("niederlassung", info);
|
||||||
|
logger.info("Niederlassung nach Reload wiederhergestellt: {}", info.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Fehler bei der Rekonstruktion der Niederlassung", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
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);
|
||||||
logger.info("Niederlassung: {}", (niederlassung != null ? niederlassung.name() : "null"));
|
logger.info("Niederlassung: {}", (niederlassung != null ? niederlassung.name() : "null"));
|
||||||
logger.info("Server: {}", server);
|
logger.info("Server: {}", server);
|
||||||
|
// Wichtig: Kein Redirect mehr hier. Falls keine Daten da sind, bleibt der Nutzer auf Main.
|
||||||
|
}
|
||||||
|
|
||||||
if (niederlassung == null) {
|
private void restoreUIStateIfNeeded() {
|
||||||
logger.error("FEHLER: Niederlassung ist null! Benutzer muss sich neu anmelden.");
|
var uiOpt = getUI();
|
||||||
getUI().ifPresent(ui -> ui.navigate("login"));
|
if (uiOpt.isEmpty())
|
||||||
|
return;
|
||||||
|
var session = uiOpt.get().getSession();
|
||||||
|
|
||||||
|
boolean emulatorStarted = Boolean.TRUE.equals(session.getAttribute("emulatorStarted"));
|
||||||
|
if (emulatorStarted && niederlassung != null) {
|
||||||
|
// UI in gestarteten Zustand versetzen
|
||||||
|
welcomeMessage.getStyle().set("display", "none");
|
||||||
|
emulatorContainer.getStyle().set("display", "block");
|
||||||
|
if (startBtn != null) {
|
||||||
|
startBtn.setEnabled(false);
|
||||||
|
startBtn.setText("Emulator gestartet");
|
||||||
|
}
|
||||||
|
// WebView aktualisieren (baut URL aus der Niederlassung)
|
||||||
|
refreshWebView();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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() {
|
||||||
@@ -236,8 +327,7 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
|
|
||||||
executor.submit(() -> {
|
executor.submit(() -> {
|
||||||
try {
|
try {
|
||||||
shutdown();
|
if (!isContainerStarted(niederlassung)) {
|
||||||
|
|
||||||
createContainer();
|
createContainer();
|
||||||
|
|
||||||
startContainer();
|
startContainer();
|
||||||
@@ -248,6 +338,8 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
|
|
||||||
installApp();
|
installApp();
|
||||||
|
|
||||||
|
startApp();
|
||||||
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
// Logging, Fehlerbehandlung …
|
// Logging, Fehlerbehandlung …
|
||||||
} finally {
|
} finally {
|
||||||
@@ -262,6 +354,9 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
// Button bleibt dauerhaft deaktiviert
|
// Button bleibt dauerhaft deaktiviert
|
||||||
startBtn.setText("Emulator gestartet");
|
startBtn.setText("Emulator gestartet");
|
||||||
|
|
||||||
|
// Zustand merken, damit Reload weiterfuehrt
|
||||||
|
ui.getSession().setAttribute("emulatorStarted", true);
|
||||||
|
|
||||||
refreshWebView();
|
refreshWebView();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -295,16 +390,16 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
|
|
||||||
String jsonPayload = """
|
String jsonPayload = """
|
||||||
{
|
{
|
||||||
"Cmd": ["curl", "-O", "https://www.appcreation.de/download/sb.apk"],
|
"Cmd": ["curl", "-O", "http://172.16.0.156:5487/downloads/sb.apk"],
|
||||||
"AttachStdout": true,
|
"AttachStdout": true,
|
||||||
"AttachStderr": true
|
"AttachStderr": true
|
||||||
}""";
|
}""";
|
||||||
|
|
||||||
// HTTP-Request erstellen
|
// HTTP-Request erstellen
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/exec"))
|
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + niederlassung.name()
|
||||||
.header("Content-Type", "application/json")
|
+ "/exec"))
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Anfrage senden und Antwort verarbeiten
|
// Anfrage senden und Antwort verarbeiten
|
||||||
@@ -324,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();
|
||||||
|
|
||||||
@@ -335,9 +507,9 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
}""";
|
}""";
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/exec"))
|
.uri(URI.create(
|
||||||
.header("Content-Type", "application/json")
|
"http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/exec"))
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -358,9 +530,7 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create("http://" + server + ":2375/exec/" + execId + "/start"))
|
.uri(URI.create("http://" + server + ":2375/exec/" + execId + "/start"))
|
||||||
.POST(HttpRequest.BodyPublishers.noBody())
|
.POST(HttpRequest.BodyPublishers.noBody()).version(HttpClient.Version.HTTP_1_1).build();
|
||||||
.version(HttpClient.Version.HTTP_1_1)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
@@ -379,7 +549,8 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
HttpClient client = HttpClient.newHttpClient();
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/start"))
|
.uri(URI.create(
|
||||||
|
"http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/start"))
|
||||||
.POST(HttpRequest.BodyPublishers.noBody()) // Kein Body wird gesendet
|
.POST(HttpRequest.BodyPublishers.noBody()) // Kein Body wird gesendet
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -407,10 +578,10 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
}""";
|
}""";
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/exec"))
|
.uri(URI.create("http://" + server + ":2375/containers/android-container-"
|
||||||
|
+ niederlassung.name() + "/exec"))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload)).build();
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
logger.info("HTTP-Response-Code: {}", response.statusCode());
|
logger.info("HTTP-Response-Code: {}", response.statusCode());
|
||||||
@@ -449,7 +620,7 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
"HostConfig": {
|
"HostConfig": {
|
||||||
"NetworkMode": "votianBridge",
|
"NetworkMode": "votianBridge",
|
||||||
"Memory": 10737418240,
|
"Memory": 10737418240,
|
||||||
"CpuCount": 4,
|
"NanoCpus": 4000000000,
|
||||||
"Dns": [
|
"Dns": [
|
||||||
"172.18.0.15"
|
"172.18.0.15"
|
||||||
],
|
],
|
||||||
@@ -494,9 +665,9 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
|
|
||||||
// HTTP-Request erstellen
|
// HTTP-Request erstellen
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create("http://" + server + ":2375/containers/create?name=android-container-" + niederlassung.name()))
|
.uri(URI.create(
|
||||||
.header("Content-Type", "application/json")
|
"http://" + server + ":2375/containers/create?name=android-container-" + niederlassung.name()))
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Anfrage senden und Antwort verarbeiten
|
// Anfrage senden und Antwort verarbeiten
|
||||||
@@ -513,7 +684,8 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
HttpClient client = HttpClient.newHttpClient();
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/stop"))
|
.uri(URI.create(
|
||||||
|
"http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/stop"))
|
||||||
.POST(HttpRequest.BodyPublishers.noBody()) // Kein Body wird gesendet
|
.POST(HttpRequest.BodyPublishers.noBody()) // Kein Body wird gesendet
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -530,7 +702,8 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
HttpClient client = HttpClient.newHttpClient();
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "?force=true"))
|
.uri(URI.create("http://" + server + ":2375/containers/android-container-" + niederlassung.name()
|
||||||
|
+ "?force=true"))
|
||||||
.DELETE() // Kein Body wird gesendet
|
.DELETE() // Kein Body wird gesendet
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -543,17 +716,100 @@ public final class MainView extends Main implements BeforeEnterObserver
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String buildContainerName(NiederlassungInfo info) {
|
||||||
|
return "android-container-" + info.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isContainerStarted(NiederlassungInfo info) {
|
||||||
|
if (info == null) {
|
||||||
|
logger.warn("isContainerStarted: Niederlassung ist null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String srv = this.server != null ? this.server : emulatorServerConfiguration.getServerIp();
|
||||||
|
String name = buildContainerName(info);
|
||||||
|
try {
|
||||||
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create("http://" + srv + ":2375/containers/" + name + "/json")).GET().build();
|
||||||
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
int status = response.statusCode();
|
||||||
|
logger.info("Inspect-Container '{}': status={} ", name, status);
|
||||||
|
if (status == 200) {
|
||||||
|
String body = response.body();
|
||||||
|
// Simple check: look for State.Running = true
|
||||||
|
boolean running = body != null && body.contains("\"Running\":true");
|
||||||
|
logger.info("Inspect-Container '{}': running={}", name, running);
|
||||||
|
return running;
|
||||||
|
} else if (status == 404) {
|
||||||
|
// Container existiert nicht
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Fehler beim Prüfen des Container-Status für '{}': {}", name, e.toString());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private Dialog showWaitDialog(String message) {
|
private Dialog showWaitDialog(String message) {
|
||||||
Dialog dialog = new Dialog();
|
Dialog dialog = new Dialog();
|
||||||
dialog.setHeaderTitle("Bitte warten");
|
|
||||||
dialog.setModal(true);
|
dialog.setModal(true);
|
||||||
dialog.setCloseOnOutsideClick(false);
|
dialog.setCloseOnOutsideClick(false);
|
||||||
dialog.setCloseOnOutsideClick(false);
|
dialog.setCloseOnEsc(false);
|
||||||
|
dialog.setDraggable(false);
|
||||||
|
dialog.setResizable(false);
|
||||||
|
|
||||||
VerticalLayout dialogLayout = new VerticalLayout(
|
// Dialog-Styling
|
||||||
new Paragraph(message)
|
dialog.getElement().getStyle().set("border-radius", "16px").set("box-shadow", "0 20px 40px rgba(0,0,0,0.3)")
|
||||||
);
|
.set("background", "rgba(255,255,255,0.95)").set("backdrop-filter", "blur(10px)");
|
||||||
dialog.add(dialogLayout);
|
|
||||||
|
// Haupt-Container
|
||||||
|
VerticalLayout mainContainer = new VerticalLayout();
|
||||||
|
mainContainer.setSpacing(false);
|
||||||
|
mainContainer.setPadding(false);
|
||||||
|
mainContainer.setAlignItems(VerticalLayout.Alignment.CENTER);
|
||||||
|
mainContainer.getStyle().set("padding", "40px").set("min-width", "400px").set("text-align", "center");
|
||||||
|
|
||||||
|
// Animiertes Icon
|
||||||
|
Icon loadingIcon = new Icon(VaadinIcon.COG);
|
||||||
|
loadingIcon.setSize("48px");
|
||||||
|
loadingIcon.getStyle().set("color", "#667eea").set("margin-bottom", "20px").set("animation",
|
||||||
|
"spin 2s linear infinite");
|
||||||
|
|
||||||
|
// CSS-Animation für das Icon
|
||||||
|
dialog.getElement().executeJs(
|
||||||
|
"""
|
||||||
|
if (!document.getElementById('spin-animation')) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'spin-animation';
|
||||||
|
style.textContent = '@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }';
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
// Titel
|
||||||
|
H3 title = new H3("Bitte warten");
|
||||||
|
title.getStyle().set("margin", "0 0 12px 0").set("color", "#333333").set("font-size", "1.5rem")
|
||||||
|
.set("font-weight", "600");
|
||||||
|
|
||||||
|
// Nachricht
|
||||||
|
Paragraph messageText = new Paragraph(message);
|
||||||
|
messageText.getStyle().set("margin", "0 0 24px 0").set("color", "#666666").set("font-size", "1rem")
|
||||||
|
.set("line-height", "1.5");
|
||||||
|
|
||||||
|
// Indeterminate Progress Bar
|
||||||
|
ProgressBar progressBar = new ProgressBar();
|
||||||
|
progressBar.setIndeterminate(true);
|
||||||
|
progressBar.setWidthFull();
|
||||||
|
progressBar.getStyle().set("margin-bottom", "16px");
|
||||||
|
|
||||||
|
// Status-Text
|
||||||
|
Paragraph statusText = new Paragraph("Vorgang wird ausgeführt...");
|
||||||
|
statusText.getStyle().set("margin", "0").set("color", "#888888").set("font-size", "0.875rem").set("font-style",
|
||||||
|
"italic");
|
||||||
|
|
||||||
|
// Alle Komponenten hinzufügen
|
||||||
|
mainContainer.add(loadingIcon, title, messageText, progressBar, statusText);
|
||||||
|
dialog.add(mainContainer);
|
||||||
|
|
||||||
dialog.open();
|
dialog.open();
|
||||||
|
|
||||||
@@ -583,20 +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());
|
||||||
if (niederlassung != null) {
|
if (ssoConfiguration.isEnabled()) {
|
||||||
Application.activeNiederlassungen.remove(niederlassung.name());
|
authenticationContext.logout();
|
||||||
logger.info("Niederlassung {} aus aktiven Niederlassungen entfernt", niederlassung.name());
|
} else {
|
||||||
|
ui.navigate(SimCardConfigurationView.class);
|
||||||
}
|
}
|
||||||
if (username != null) {
|
|
||||||
Application.activeUsers.remove(username);
|
|
||||||
logger.info("Benutzer {} aus aktiven Benutzern entfernt", username);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session invalidieren und weiterleiten erst ganz am Ende
|
|
||||||
ui.getSession().getSession().invalidate();
|
|
||||||
logger.info("Session invalidiert - Weiterleitung zur Login-Seite");
|
|
||||||
ui.getPage().setLocation("login");
|
|
||||||
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
logger.error("Fehler beim Logout UI-Update", ex);
|
logger.error("Fehler beim Logout UI-Update", ex);
|
||||||
|
|||||||
@@ -3,14 +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,33 +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.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
|
http.sessionManagement(session -> session
|
||||||
.logout(logout -> logout
|
// Wichtig: Session-Fixation so konfigurieren, dass Session-Attribute (z.B. "user") erhalten bleiben
|
||||||
.logoutSuccessUrl("/login")
|
.sessionFixation(fixation -> fixation.migrateSession()).maximumSessions(Application.MAX_ACTIVE_SESSIONS)
|
||||||
|
.maxSessionsPreventsLogin(false));
|
||||||
|
|
||||||
|
if (ssoConfiguration.isEnabled()) {
|
||||||
|
LogoutSuccessHandler logoutSuccessHandler = oidcLogoutSuccessHandler("{baseUrl}/login");
|
||||||
|
|
||||||
|
http.oauth2Login(
|
||||||
|
oauth2 -> oauth2.loginPage("/login").defaultSuccessUrl("/login", true).failureUrl("/login?error"));
|
||||||
|
|
||||||
|
http.logout(logout -> {
|
||||||
|
logout.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET")).invalidateHttpSession(true)
|
||||||
|
.clearAuthentication(true).deleteCookies("JSESSIONID")
|
||||||
.addLogoutHandler((request, response, authentication) -> {
|
.addLogoutHandler((request, response, authentication) -> {
|
||||||
if (authentication != null) {
|
var session = request.getSession(false);
|
||||||
String username = authentication.getName();
|
if (session != null) {
|
||||||
Application.activeNiederlassungen.entrySet().removeIf(entry ->
|
loginSessionService.cleanupApplicationSession(session.getId());
|
||||||
entry.getValue().equals(username));
|
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
);
|
|
||||||
|
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,15 @@ 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");
|
||||||
|
|
||||||
if (username != null && niederlassung != null) {
|
if (niederlassung != null) {
|
||||||
Application.activeNiederlassungen.remove(niederlassung.name());
|
Application.activeNiederlassungen.remove(niederlassung.name());
|
||||||
}
|
}
|
||||||
|
if (sessionId != null) {
|
||||||
|
Application.activeSessions.remove(sessionId);
|
||||||
|
Application.sessionsById.remove(sessionId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,56 +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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,11 @@ public class ExecResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Getter und Setter
|
// Getter und Setter
|
||||||
public String getId() { return id; }
|
public String getId() {
|
||||||
public void setId(String id) { this.id = id; }
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,7 @@ package de.assecutor.emulatorstation.pojo;
|
|||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
public record NiederlassungInfo(String name, String ip, String port, String urlExtension)
|
public record NiederlassungInfo(String name, String ip, String port, String urlExtension) implements Serializable {
|
||||||
implements Serializable {
|
|
||||||
|
|
||||||
@Serial
|
@Serial
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,5 +2,3 @@ package de.assecutor.emulatorstation.pojo;
|
|||||||
|
|
||||||
public record UserInfo(String password) {
|
public record UserInfo(String password) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,15 @@ package util;
|
|||||||
|
|
||||||
import com.vaadin.flow.server.WrappedSession;
|
import com.vaadin.flow.server.WrappedSession;
|
||||||
|
|
||||||
|
|
||||||
public class Util {
|
public class Util {
|
||||||
public static String getSessionAttributeWithDefault(WrappedSession currentSession, String key, String defaultValue) {
|
public static String getSessionAttributeWithDefault(WrappedSession currentSession, String key,
|
||||||
|
String defaultValue) {
|
||||||
var result = defaultValue;
|
var result = defaultValue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = currentSession.getAttribute(key).toString();
|
result = currentSession.getAttribute(key).toString();
|
||||||
} catch (Exception e) {}
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/main/resources/application-prod.properties
Normal file
7
src/main/resources/application-prod.properties
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Production overrides
|
||||||
|
# - Secure session cookie only over HTTPS
|
||||||
|
# - SameSite policy for CSRF protection and to avoid third-party sending
|
||||||
|
|
||||||
|
server.servlet.session.cookie.secure=true
|
||||||
|
server.servlet.session.cookie.same-site=lax
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -13,5 +16,20 @@ spring.jpa.open-in-view=false
|
|||||||
# Initialize the JPA Entity Manager before considering data.sql so that the EM can create the schema and data.sql contain data
|
# Initialize the JPA Entity Manager before considering data.sql so that the EM can create the schema and data.sql contain data
|
||||||
spring.jpa.defer-datasource-initialization = true
|
spring.jpa.defer-datasource-initialization = true
|
||||||
|
|
||||||
server.servlet.session.timeout=300s
|
# Session configuration - preserve session on browser reload
|
||||||
|
server.servlet.session.timeout=0
|
||||||
|
|
||||||
|
server.servlet.session.cookie.max-age=180d
|
||||||
|
|
||||||
|
server.servlet.session.cookie.http-only=true
|
||||||
|
server.servlet.session.cookie.secure=false
|
||||||
|
server.servlet.session.persistent=true
|
||||||
|
server.forward-headers-strategy=framework
|
||||||
|
|
||||||
|
# Vaadin session configuration
|
||||||
vaadin.heartbeatInterval=300
|
vaadin.heartbeatInterval=300
|
||||||
|
vaadin.closeIdleSessions=false
|
||||||
|
|
||||||
|
|
||||||
|
# Disable Spring Boot's default generated user/password (auth is handled via Azure AD SSO)
|
||||||
|
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package de.assecutor.emulatorstation.base.domain;
|
||||||
|
|
||||||
|
import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
class NiederlassungResolverTest {
|
||||||
|
|
||||||
|
private final NiederlassungResolver niederlassungResolver = new NiederlassungResolver();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolvesNiederlassungFromKnownSimCodes() {
|
||||||
|
assertNiederlassung("EMU_B", "Berlin");
|
||||||
|
assertNiederlassung("EMU_HB", "Bremen");
|
||||||
|
assertNiederlassung("EMU_HH", "Hamburg");
|
||||||
|
assertNiederlassung("EMU_E", "Essen");
|
||||||
|
assertNiederlassung("EMU_L", "Leipzig");
|
||||||
|
assertNiederlassung("EMU_DD", "Dresden");
|
||||||
|
assertNiederlassung("EMU_H", "Hannover");
|
||||||
|
assertNiederlassung("EMU_S", "Stuttgart");
|
||||||
|
assertNiederlassung("EMU_F", "Frankfurt am Main");
|
||||||
|
assertNiederlassung("EMU_GFL", "Geschäftführung");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolvesSimCodesCaseInsensitive() {
|
||||||
|
assertEquals("HH", niederlassungResolver.extractCode("emu_hh"));
|
||||||
|
assertNiederlassung(" emu_b ", "Berlin");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsNullForUnsupportedSimNames() {
|
||||||
|
assertNull(niederlassungResolver.extractCode("EMULATOR172_16_0_158"));
|
||||||
|
assertNull(niederlassungResolver.resolveFromSecretValue("EMU_X"));
|
||||||
|
assertNull(niederlassungResolver.resolveFromSecretValue(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertNiederlassung(String secretValue, String expectedName) {
|
||||||
|
NiederlassungInfo niederlassungInfo = niederlassungResolver.resolveFromSecretValue(secretValue);
|
||||||
|
assertEquals(expectedName, niederlassungInfo.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
39
tsconfig.json
Normal file
39
tsconfig.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// This TypeScript configuration file is generated by vaadin-maven-plugin.
|
||||||
|
// This is needed for TypeScript compiler to compile your TypeScript code in the project.
|
||||||
|
// It is recommended to commit this file to the VCS.
|
||||||
|
// You might want to change the configurations to fit your preferences
|
||||||
|
// For more information about the configurations, please refer to http://www.typescriptlang.org/docs/handbook/tsconfig-json.html
|
||||||
|
{
|
||||||
|
"_version": "9.1",
|
||||||
|
"compilerOptions": {
|
||||||
|
"sourceMap": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"inlineSources": true,
|
||||||
|
"module": "esNext",
|
||||||
|
"target": "es2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"baseUrl": "src/main/frontend",
|
||||||
|
"paths": {
|
||||||
|
"@vaadin/flow-frontend": ["generated/jar-resources"],
|
||||||
|
"@vaadin/flow-frontend/*": ["generated/jar-resources/*"],
|
||||||
|
"Frontend/*": ["*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/main/frontend/**/*",
|
||||||
|
"types.d.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/main/frontend/generated/jar-resources/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
17
types.d.ts
vendored
Normal file
17
types.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// This TypeScript modules definition file is generated by vaadin-maven-plugin.
|
||||||
|
// You can not directly import your different static files into TypeScript,
|
||||||
|
// This is needed for TypeScript compiler to declare and export as a TypeScript module.
|
||||||
|
// It is recommended to commit this file to the VCS.
|
||||||
|
// You might want to change the configurations to fit your preferences
|
||||||
|
declare module '*.css?inline' {
|
||||||
|
import type { CSSResultGroup } from 'lit';
|
||||||
|
const content: CSSResultGroup;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow any CSS Custom Properties
|
||||||
|
declare module 'csstype' {
|
||||||
|
interface Properties {
|
||||||
|
[index: `--${string}`]: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user