Compare commits

..

10 Commits

Author SHA1 Message Date
f764b4a7aa Erweiterungen 2025-11-04 11:25:20 +01:00
297d0cf000 Erweiterungen 2025-11-04 11:24:09 +01:00
0e28a388b7 Erweiterungen 2025-10-28 10:25:04 +01:00
5fbf05b420 Erweiterungen 2025-10-20 10:08:35 +02:00
f3f8f90737 Erweiterungen 2025-10-01 17:36:34 +02:00
479eb5a65a Erweiterungen 2025-10-01 17:31:26 +02:00
ddfb8a692b Erweiterungen 2025-10-01 17:28:04 +02:00
0747d131ee Erweiterungen 2025-09-19 09:43:34 +02:00
562e9bf16e Erweiterungen 2025-09-18 20:26:41 +02:00
5b0be56914 Erweiterungen 2025-09-18 20:16:49 +02:00
21 changed files with 10366 additions and 266 deletions

9513
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

110
package.json Normal file
View 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"
}
}

View File

@@ -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.13</version>
<packaging>jar</packaging> <packaging>jar</packaging>

Binary file not shown.

View File

@@ -10,17 +10,18 @@ import java.util.concurrent.ConcurrentHashMap;
import de.assecutor.emulatorstation.pojo.UserInfo; import de.assecutor.emulatorstation.pojo.UserInfo;
import de.assecutor.emulatorstation.pojo.NiederlassungInfo; import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
import org.springframework.scheduling.annotation.EnableScheduling;
@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( // Single user configuration
Map.entry("user1", new UserInfo("pass1")), public static final String SINGLE_USERNAME = "user";
Map.entry("user2", new UserInfo("pass2")), public static final String SINGLE_PASSWORD = "user123";
Map.entry("user3", new UserInfo("pass3")), public static final int MAX_ACTIVE_SESSIONS = 5;
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")),
@@ -32,12 +33,11 @@ public class Application implements AppShellConfigurator {
Map.entry("Hannover", new NiederlassungInfo("Hannover", "172.18.0.104", "6084", "/hannover")), Map.entry("Hannover", new NiederlassungInfo("Hannover", "172.18.0.104", "6084", "/hannover")),
Map.entry("Stuttgart", new NiederlassungInfo("Stuttgart", "172.18.0.111", "6091", "/stuttgart")), Map.entry("Stuttgart", new NiederlassungInfo("Stuttgart", "172.18.0.111", "6091", "/stuttgart")),
Map.entry("Frankfurt am Main", new NiederlassungInfo("Frankfurt am Main", "172.18.0.105", "6085", "/frankfurt")), Map.entry("Frankfurt am Main", new NiederlassungInfo("Frankfurt am Main", "172.18.0.105", "6085", "/frankfurt")),
Map.entry("Geschäftführung", new NiederlassungInfo("Geschäftführung", "172.18.0.112", "6092", "/gfl")), Map.entry("Geschäftführung", new NiederlassungInfo("Geschäftführung", "172.18.0.112", "6092", "/gfl")));
Map.entry("Admin", new NiederlassungInfo("Admin", "172.18.0.110", "6090", "/admin"))
);
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);

View File

@@ -0,0 +1,99 @@
package de.assecutor.emulatorstation.base.domain;
import de.assecutor.emulatorstation.Application;
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 static final String DOCKER_HOST = "172.16.0.158"; // gleiches Fallback wie in MainView/LoginView
private final HttpClient http = HttpClient.newHttpClient();
// 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 {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("http://" + DOCKER_HOST + ":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 {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("http://" + DOCKER_HOST + ":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());
}
}
}

View File

@@ -1,13 +1,16 @@
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.html.Div;
import com.vaadin.flow.component.login.LoginForm; import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.select.Select; 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.textfield.PasswordField;
import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
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 de.assecutor.emulatorstation.Application;
@@ -17,6 +20,8 @@ import org.springframework.security.core.context.SecurityContextHolder;
import java.util.List; import java.util.List;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
@Route("login") @Route("login")
@AnonymousAllowed @AnonymousAllowed
@@ -25,52 +30,75 @@ public class LoginView extends VerticalLayout {
private static final Logger logger = LoggerFactory.getLogger(LoginView.class); private static final Logger logger = LoggerFactory.getLogger(LoginView.class);
public LoginView() { public LoginView() {
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"); // Haupt-Container für das Login-Formular
title.getStyle().set("font-size", "24px").set("font-weight", "bold"); Div loginContainer = new Div();
loginContainer.getStyle().set("background-color", "#ffffff").set("border-radius", "12px")
.set("box-shadow", "0 10px 30px rgba(0,0,0,0.2)").set("padding", "40px").set("max-width", "400px")
.set("width", "100%").set("text-align", "center");
TextField usernameField = new TextField("Benutzername"); // Logo/Icon
Icon icon = new Icon(VaadinIcon.DESKTOP);
icon.setSize("64px");
icon.getStyle().set("color", "#667eea").set("margin-bottom", "20px");
// Titel
H1 title = new H1("Emulator Station");
title.getStyle().set("margin", "0 0 8px 0").set("color", "#333333").set("font-size", "2rem").set("font-weight",
"600");
// Untertitel
Paragraph subtitle = new Paragraph("Melden Sie sich an, um fortzufahren");
subtitle.getStyle().set("margin", "0 0 30px 0").set("color", "#666666").set("font-size", "0.95rem");
// Form-Container
VerticalLayout formLayout = new VerticalLayout();
formLayout.setSpacing(true);
formLayout.setPadding(false);
formLayout.setWidthFull();
// Eingabefelder - nur noch Passwort
PasswordField passwordField = new PasswordField("Passwort"); PasswordField passwordField = new PasswordField("Passwort");
passwordField.setWidthFull();
passwordField.getStyle().set("margin-bottom", "16px");
Select<String> niederlassungSelect = new Select<>(); Select<String> niederlassungSelect = new Select<>();
niederlassungSelect.setLabel("Niederlassung"); niederlassungSelect.setLabel("Niederlassung");
niederlassungSelect.setItems(Application.niederlassungen.keySet().stream().sorted().toList()); niederlassungSelect.setItems(Application.niederlassungen.keySet().stream().sorted().toList());
niederlassungSelect.setPlaceholder("Wählen Sie eine Niederlassung"); niederlassungSelect.setPlaceholder("Wählen Sie eine Niederlassung");
niederlassungSelect.setWidthFull();
niederlassungSelect.getStyle().set("margin-bottom", "24px");
Button loginButton = new Button("Anmelden", event -> { Button loginButton = new Button("Anmelden", new Icon(VaadinIcon.SIGN_IN), event -> {
String username = usernameField.getValue();
String password = passwordField.getValue(); String password = passwordField.getValue();
String niederlassung = niederlassungSelect.getValue(); String niederlassung = niederlassungSelect.getValue();
if (username.isEmpty() || password.isEmpty() || niederlassung == null) { // Validierung der Eingabefelder
if (password.isEmpty() || niederlassung == null) {
Notification.show("Bitte alle Felder ausfüllen", 3000, Notification.Position.MIDDLE); Notification.show("Bitte alle Felder ausfüllen", 3000, Notification.Position.MIDDLE);
return; return;
} }
if (!Application.users.containsKey(username) || // Passwort prüfen
!Application.users.get(username).password().equals(password)) { if (!Application.SINGLE_PASSWORD.equals(password)) {
Notification.show("Ungültige Anmeldedaten", 3000, Notification.Position.MIDDLE); Notification.show("Ungültiges Passwort", 3000, Notification.Position.MIDDLE);
return; return;
} }
if (Application.activeUsers.containsKey(username)) { // Maximale Session-Anzahl prüfen
Notification.show("Benutzer ist bereits angemeldet", 3000, Notification.Position.MIDDLE); if (Application.activeSessions.size() >= Application.MAX_ACTIVE_SESSIONS) {
Notification.show(
"Maximale Anzahl von " + Application.MAX_ACTIVE_SESSIONS
+ " gleichzeitigen Anmeldungen erreicht. Bitte versuchen Sie es später erneut.",
5000, Notification.Position.MIDDLE);
return; 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); var niederlassungInfo = Application.niederlassungen.get(niederlassung);
if (niederlassungInfo == null) { if (niederlassungInfo == null) {
@@ -79,22 +107,120 @@ public class LoginView extends VerticalLayout {
} }
getUI().ifPresent(ui -> { getUI().ifPresent(ui -> {
ui.getSession().setAttribute("user", username); if (isContainerRunning(niederlassungInfo)) {
ui.getSession().setAttribute("username", username); var dialog = new com.vaadin.flow.component.dialog.Dialog();
ui.getSession().setAttribute("niederlassung", niederlassungInfo); dialog.setHeaderTitle("Container läuft bereits");
var text = new Paragraph("Für '" + niederlassungInfo.name() + "' läuft bereits ein Emulator-Container. Möchten Sie ihn für diese Session übernehmen?");
logger.info("Login erfolgreich - Session-Daten gesetzt:"); var adopt = new Button("Übernehmen", e2 -> {
logger.info("Username: {}", username); dialog.close();
logger.info("Niederlassung: {}", niederlassungInfo.name()); completeLogin(ui, password, niederlassungInfo, true);
});
ui.navigate("main"); adopt.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
var cancel = new Button("Abbrechen", e2 -> dialog.close());
var buttons = new com.vaadin.flow.component.orderedlayout.HorizontalLayout(cancel, adopt);
buttons.setWidthFull();
buttons.setJustifyContentMode(com.vaadin.flow.component.orderedlayout.FlexComponent.JustifyContentMode.END);
dialog.add(text, buttons);
dialog.open();
} else {
completeLogin(ui, password, niederlassungInfo, false);
}
}); });
}); });
add(title, usernameField, passwordField, niederlassungSelect, loginButton); // Button-Styling
loginButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE);
loginButton.setWidthFull();
loginButton.getStyle().set("margin-top", "8px")
.set("background", "linear-gradient(135deg, #667eea 0%, #764ba2 100%)").set("border", "none")
.set("font-weight", "600");
// Eingabefelder zum Form-Layout hinzufügen
formLayout.add(passwordField, niederlassungSelect, loginButton);
// Alle Komponenten zum Login-Container hinzufügen
loginContainer.add(icon, title, subtitle, formLayout);
// Login-Container zur Hauptansicht hinzufügen
add(loginContainer);
if (MainLayout.instance != null) { if (MainLayout.instance != null) {
MainLayout.instance.setDrawerOpened(false); MainLayout.instance.setDrawerOpened(false);
} }
} }
private boolean isContainerRunning(de.assecutor.emulatorstation.pojo.NiederlassungInfo info) {
if (info == null) {
return false;
}
final String srv = "172.16.0.158"; // Fallback wie in MainView
final String name = "android-container-" + info.name();
try {
var client = java.net.http.HttpClient.newHttpClient();
var request = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://" + srv + ":2375/containers/" + name + "/json"))
.GET()
.build();
var response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
var body = response.body();
return body != null && body.contains("\"Running\":true");
}
} catch (Exception e) {
logger.warn("Fehler beim Pruefen des Container-Status fuer '{}': {}", name, e.toString());
}
return false;
}
private void completeLogin(com.vaadin.flow.component.UI ui, String password,
de.assecutor.emulatorstation.pojo.NiederlassungInfo niederlassungInfo,
boolean adoptRunningContainer) {
String niederlassung = niederlassungInfo.name();
String sessionId = ui.getSession().getSession().getId();
// Session niemals ablaufen lassen
ui.getSession().getSession().setMaxInactiveInterval(-1);
// HttpSession/WrappedSession im Registry merken, um später invalidieren zu können
Application.sessionsById.put(sessionId, ui.getSession().getSession());
// Session registrieren
Application.activeSessions.put(sessionId, niederlassung);
Application.activeNiederlassungen.put(niederlassung, sessionId);
// Log that Niederlassung is now blocked
logger.info("Niederlassung '{}' wurde gesperrt fuer Session {}", niederlassung, sessionId);
logger.info("Aktive Niederlassungen: {}", Application.activeNiederlassungen.keySet());
// Spring Security Authentifizierung setzen
var authorities = java.util.List.of(new org.springframework.security.core.authority.SimpleGrantedAuthority("ROLE_USER"));
var authentication = new org.springframework.security.authentication.UsernamePasswordAuthenticationToken(
Application.SINGLE_USERNAME, password, authorities);
org.springframework.security.core.context.SecurityContextHolder.getContext().setAuthentication(authentication);
// Spring Security Kontext explizit in der HTTP-Session speichern, damit Reload eingeloggt bleibt
ui.getSession().getSession().setAttribute(
org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
org.springframework.security.core.context.SecurityContextHolder.getContext());
// Session-Daten setzen
ui.getSession().setAttribute("user", Application.SINGLE_USERNAME);
ui.getSession().setAttribute("username", Application.SINGLE_USERNAME);
ui.getSession().setAttribute("niederlassung", niederlassungInfo);
ui.getSession().setAttribute("sessionId", sessionId);
if (adoptRunningContainer) {
// UI soll direkt in gestarteten Zustand gehen
ui.getSession().setAttribute("emulatorStarted", true);
}
logger.info("Login erfolgreich - Session-Daten gesetzt:");
logger.info("Username: {}", Application.SINGLE_USERNAME);
logger.info("Niederlassung: {}", niederlassungInfo.name());
logger.info("SessionId: {}", sessionId);
logger.info("Aktive Sessions: {}/{}", Application.activeSessions.size(), Application.MAX_ACTIVE_SESSIONS);
ui.navigate("main");
}
} }

View File

@@ -42,24 +42,17 @@ 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 // TODO Replace with real application logo and name
var appLogo = VaadinIcon.CUBES.create(); var appLogo = VaadinIcon.CUBES.create();

View File

@@ -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,7 +20,10 @@ 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.auth.AnonymousAllowed; import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.flow.server.VaadinSession;
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;
@@ -35,9 +40,9 @@ import java.util.concurrent.Executors;
@PermitAll // When security is enabled, allow all authenticated users @PermitAll // When security is enabled, allow all authenticated users
@Route("main") @Route("main")
@PreserveOnRefresh
@AnonymousAllowed @AnonymousAllowed
public final class MainView extends Main implements BeforeEnterObserver public final class MainView extends Main implements BeforeEnterObserver {
{
private static final Logger logger = LoggerFactory.getLogger(MainView.class); private static final Logger logger = LoggerFactory.getLogger(MainView.class);
private String username; private String username;
private NiederlassungInfo niederlassung; private NiederlassungInfo niederlassung;
@@ -52,69 +57,104 @@ public final class MainView extends Main implements BeforeEnterObserver
MainView() { MainView() {
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 +164,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 +175,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 +205,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() {
@@ -201,6 +227,29 @@ public final class MainView extends Main implements BeforeEnterObserver
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"); 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");
} }
@@ -213,10 +262,25 @@ public final class MainView extends Main implements BeforeEnterObserver
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();
} }
} }
@@ -236,8 +300,7 @@ public final class MainView extends Main implements BeforeEnterObserver
executor.submit(() -> { executor.submit(() -> {
try { try {
shutdown(); if (!isContainerStarted(niederlassung)) {
createContainer(); createContainer();
startContainer(); startContainer();
@@ -247,7 +310,7 @@ public final class MainView extends Main implements BeforeEnterObserver
downloadApp(); downloadApp();
installApp(); installApp();
}
} catch (Exception ex) { } catch (Exception ex) {
// Logging, Fehlerbehandlung … // Logging, Fehlerbehandlung …
} finally { } finally {
@@ -262,6 +325,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 +361,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
@@ -335,9 +401,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 +424,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 +443,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 +472,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 +514,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 +559,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 +578,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 +596,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();
@@ -542,18 +609,103 @@ public final class MainView extends Main implements BeforeEnterObserver
logger.error("Fehler beim HTTP-Request", e); logger.error("Fehler beim HTTP-Request", e);
} }
} }
private String buildContainerName(NiederlassungInfo info) {
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 : "172.16.0.158");
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();
@@ -584,19 +736,44 @@ public final class MainView extends Main implements BeforeEnterObserver
dialog.close(); dialog.close();
// Session cleanup erst nach Shutdown // Session cleanup erst nach Shutdown
String sessionId = (String) ui.getSession().getAttribute("sessionId");
if (sessionId != null) {
Application.activeSessions.remove(sessionId);
logger.info("Session {} aus aktiven Sessions entfernt", sessionId);
Application.sessionsById.remove(sessionId);
}
if (niederlassung != null) { if (niederlassung != null) {
Application.activeNiederlassungen.remove(niederlassung.name()); Application.activeNiederlassungen.remove(niederlassung.name());
logger.info("Niederlassung {} aus aktiven Niederlassungen entfernt", niederlassung.name()); logger.info("Niederlassung {} aus aktiven Niederlassungen entfernt",
} niederlassung.name());
if (username != null) {
Application.activeUsers.remove(username);
logger.info("Benutzer {} aus aktiven Benutzern entfernt", username);
} }
logger.info("Aktive Sessions nach Logout: {}/{}", Application.activeSessions.size(),
Application.MAX_ACTIVE_SESSIONS);
// Session invalidieren und weiterleiten erst ganz am Ende // 1) Client-seitigen Redirect sofort ausführen (wird beim Client direkt verarbeitet)
ui.getSession().getSession().invalidate(); var httpSession = ui.getSession().getSession();
logger.info("Session invalidiert - Weiterleitung zur Login-Seite"); var vaadinSession = ui.getSession();
ui.getPage().setLocation("login"); ui.getPage().executeJs("window.location.replace($0);", "login");
// 2) Session-Schließung leicht verzögert im Hintergrund, damit der Redirect sicher ankommt
executor.submit(() -> {
try {
Thread.sleep(300); // kurze Verzögerung reicht i.d.R.
} catch (InterruptedException ignored) {
}
try {
vaadinSession.close();
} catch (Exception e) {
logger.warn("Fehler beim Schließen der VaadinSession nach Redirect", e);
}
try {
httpSession.invalidate();
} catch (Exception e) {
logger.warn("Fehler beim Invalidieren der HttpSession nach Redirect", e);
}
logger.info("Session nach Redirect geschlossen/invalidiert");
});
} catch (Exception ex) { } catch (Exception ex) {
logger.error("Fehler beim Logout UI-Update", ex); logger.error("Fehler beim Logout UI-Update", ex);

View File

@@ -12,5 +12,3 @@ public class RootView implements BeforeEnterObserver {
event.rerouteTo("main"); event.rerouteTo("main");
} }
} }

View File

@@ -4,6 +4,7 @@ import com.vaadin.flow.spring.security.VaadinWebSecurity;
import de.assecutor.emulatorstation.base.ui.view.LoginView; import de.assecutor.emulatorstation.base.ui.view.LoginView;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
import de.assecutor.emulatorstation.Application; import de.assecutor.emulatorstation.Application;
@@ -16,19 +17,19 @@ public class SecurityConfig extends VaadinWebSecurity {
setLoginView(http, LoginView.class); setLoginView(http, LoginView.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())
.addLogoutHandler((request, response, authentication) -> { .maximumSessions(Application.MAX_ACTIVE_SESSIONS)
.maxSessionsPreventsLogin(false))
.logout(
logout -> logout.logoutSuccessUrl("/login").addLogoutHandler((request, response, authentication) -> {
if (authentication != null) { if (authentication != null) {
String username = authentication.getName(); String username = authentication.getName();
Application.activeNiederlassungen.entrySet().removeIf(entry -> Application.activeNiederlassungen.entrySet()
entry.getValue().equals(username)); .removeIf(entry -> entry.getValue().equals(username));
} }
}) }));
);
} }
} }

View File

@@ -13,9 +13,14 @@ public class SessionListener implements SessionDestroyListener {
public void sessionDestroy(SessionDestroyEvent event) { public void sessionDestroy(SessionDestroyEvent event) {
String username = (String) event.getSession().getAttribute("user"); 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 (username != null && niederlassung != null) {
Application.activeNiederlassungen.remove(niederlassung.name()); Application.activeNiederlassungen.remove(niederlassung.name());
} }
if (sessionId != null) {
Application.activeSessions.remove(sessionId);
Application.sessionsById.remove(sessionId);
}
} }
} }

View File

@@ -52,5 +52,3 @@ public class VaadinAccessHandler {
} }
} }
} }

View File

@@ -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;
}
} }

View File

@@ -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;

View File

@@ -2,5 +2,3 @@ package de.assecutor.emulatorstation.pojo;
public record UserInfo(String password) { public record UserInfo(String password) {
} }

View File

@@ -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;
} }

View 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

View File

@@ -13,5 +13,19 @@ 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
# Vaadin session configuration
vaadin.heartbeatInterval=300 vaadin.heartbeatInterval=300
vaadin.closeIdleSessions=false
# Disable Spring Boot's default generated user/password (we handle auth via Vaadin & custom login)
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration

39
tsconfig.json Normal file
View 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
View 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;
}
}