Erweiterungen

This commit is contained in:
2025-09-18 17:49:10 +02:00
parent 81932cf0dc
commit 4a84ac8fa4
10 changed files with 294 additions and 105 deletions

View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(mvn compile:*)"
],
"deny": [],
"ask": []
}
}

View File

@@ -6,7 +6,7 @@
<groupId>de.assecutor.emulatorstation</groupId>
<artifactId>emulatorstation</artifactId>
<version>0.9.6</version>
<version>0.9.7</version>
<packaging>jar</packaging>

Binary file not shown.

View File

@@ -6,26 +6,38 @@ import com.vaadin.flow.theme.Theme;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import de.assecutor.emulatorstation.pojo.UserInfo;
import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
@SpringBootApplication
@Push
@Theme("default")
public class Application implements AppShellConfigurator {
public static final Map<String, UserInfo> users = Map.ofEntries(
Map. entry("admin", new UserInfo("ZY6X9X93Co8m", null, null, null)),
Map.entry("GFL", new UserInfo("GFL123", "172.18.0.112", "6092", "/gfl")),
Map.entry("Berlin", new UserInfo("Berlin123", "172.18.0.103", "6083", "/berlin")),
Map.entry("Bremen", new UserInfo("Bremen123", "172.18.0.101", "6081", "/bremen")),
Map.entry("Hamburg", new UserInfo("Hamburg123", "172.18.0.102", "6082", "/hamburg")),
Map.entry("Essen", new UserInfo("Essen123", "172.18.0.107", "6087", "/essen")),
Map.entry("Leipzig", new UserInfo("Leipzig123", "172.18.0.108", "6088", "/leipzig")),
Map.entry("Dresden", new UserInfo("Dresden123", "172.18.0.106", "6086", "/dresden")),
Map.entry("Hannover", new UserInfo("Hannover123", "172.18.0.104", "6084", "")),
Map.entry("Stuttgart", new UserInfo("Stuttgart123", "172.18.0.111", "6091", "/stuttgart")),
Map.entry("FrankfurtAmMain", new UserInfo("FrankfurtAmMain123", "172.18.0.105", "6085", "/frankfurt"))
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(
Map.entry("Berlin", new NiederlassungInfo("Berlin", "172.18.0.103", "6083", "/berlin")),
Map.entry("Bremen", new NiederlassungInfo("Bremen", "172.18.0.101", "6081", "/bremen")),
Map.entry("Hamburg", new NiederlassungInfo("Hamburg", "172.18.0.102", "6082", "/hamburg")),
Map.entry("Essen", new NiederlassungInfo("Essen", "172.18.0.107", "6087", "/essen")),
Map.entry("Leipzig", new NiederlassungInfo("Leipzig", "172.18.0.108", "6088", "/leipzig")),
Map.entry("Dresden", new NiederlassungInfo("Dresden", "172.18.0.106", "6086", "/dresden")),
Map.entry("Hannover", new NiederlassungInfo("Hannover", "172.18.0.104", "6084", "/hannover")),
Map.entry("Stuttgart", new NiederlassungInfo("Stuttgart", "172.18.0.111", "6091", "/stuttgart")),
Map.entry("Frankfurt am Main", new NiederlassungInfo("Frankfurt am Main", "172.18.0.105", "6085", "/frankfurt")),
Map.entry("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 Map<String, String> activeNiederlassungen = new ConcurrentHashMap<>();
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

View File

@@ -3,8 +3,18 @@ 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.textfield.TextField;
import com.vaadin.flow.component.textfield.PasswordField;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import de.assecutor.emulatorstation.Application;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.List;
@Route("login")
@AnonymousAllowed
@@ -16,11 +26,56 @@ public class LoginView extends VerticalLayout {
Span title = new Span("Anmelden");
title.getStyle().set("font-size", "24px").set("font-weight", "bold");
LoginForm loginForm = new LoginForm();
loginForm.setAction("login");
loginForm.setForgotPasswordButtonVisible(false);
TextField usernameField = new TextField("Benutzername");
PasswordField passwordField = new PasswordField("Passwort");
add(title, loginForm);
Select<String> niederlassungSelect = new Select<>();
niederlassungSelect.setLabel("Niederlassung");
niederlassungSelect.setItems(Application.niederlassungen.keySet().stream().sorted().toList());
niederlassungSelect.setPlaceholder("Wählen Sie eine Niederlassung");
Button loginButton = new Button("Anmelden", event -> {
String username = usernameField.getValue();
String password = passwordField.getValue();
String niederlassung = niederlassungSelect.getValue();
if (username.isEmpty() || password.isEmpty() || niederlassung == null) {
Notification.show("Bitte alle Felder ausfüllen", 3000, Notification.Position.MIDDLE);
return;
}
if (!Application.users.containsKey(username) ||
!Application.users.get(username).password().equals(password)) {
Notification.show("Ungültige Anmeldedaten", 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);
// Spring Security Authentifizierung setzen
var authorities = List.of(new SimpleGrantedAuthority("ROLE_USER"));
var authentication = new UsernamePasswordAuthenticationToken(username, password, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
getUI().ifPresent(ui -> {
ui.getSession().setAttribute("user", username);
ui.getSession().setAttribute("username", username);
ui.getSession().setAttribute("niederlassung", niederlassung);
System.out.println("Login erfolgreich - Session-Daten gesetzt:");
System.out.println("Username: " + username);
System.out.println("Niederlassung: " + niederlassung);
ui.navigate("main");
});
});
add(title, usernameField, passwordField, niederlassungSelect, loginButton);
if (MainLayout.instance != null) {
MainLayout.instance.setDrawerOpened(false);

View File

@@ -4,8 +4,12 @@ import com.vaadin.flow.component.ClientCallable;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.IFrame;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.BeforeEnterEvent;
@@ -16,11 +20,11 @@ import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.emulatorstation.pojo.ExecResponse;
import de.assecutor.emulatorstation.Application;
import jakarta.annotation.security.PermitAll;
import util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.net.http.HttpClient;
@@ -34,25 +38,46 @@ import java.util.concurrent.Executors;
@AnonymousAllowed
public final class MainView extends Main implements BeforeEnterObserver
{
private final String username;
private final String server;
private static final Logger logger = LoggerFactory.getLogger(MainView.class);
private String username;
private String niederlassung;
private String server;
private final IFrame webView;
private final IFrame webView = new IFrame();
private final Div emulatorContainer = new Div();
private final Div welcomeMessage = new Div();
private final ExecutorService executor = Executors.newSingleThreadExecutor();
MainView() {
try {
} catch (Exception ex) {
showWaitDialog("Fehler!\n" + ex.toString());
setSizeFull();
getStyle()
.set("display", "flex")
.set("flex-direction", "column")
.set("align-items", "center")
.set("justify-content", "center")
.set("padding", "5%")
.set("box-sizing", "border-box")
.set("overflow", "hidden");
var contentContainer = new Div();
contentContainer.getStyle()
.set("width", "95%")
.set("height", "95%")
.set("display", "flex")
.set("flex-direction", "column")
.set("box-sizing", "border-box");
setupInactivityTimer();
setupWelcomeMessage(contentContainer);
setupEmulatorContainer(contentContainer);
setupButtonLayout(contentContainer);
add(contentContainer);
addAttachListener(event -> loadSessionData());
}
var currentSession = VaadinService.getCurrentRequest().getWrappedSession();
username = Util.getSessionAttributeWithDefault(currentSession, "username", null);
server = Util.getSessionAttributeWithDefault(currentSession, "server", "172.16.0.158");
addClassName(LumoUtility.Padding.MEDIUM);
private void setupInactivityTimer() {
String jsCode = """
var inactivityTime = function () {
var time;
@@ -63,41 +88,106 @@ public final class MainView extends Main implements BeforeEnterObserver
document.onscroll = resetTimer;
function logout() {
$0.$server.logout(); // Server-Logout via RPC
$0.$server.logout();
}
function resetTimer() {
clearTimeout(time);
time = setTimeout(logout, 5 * 60 * 1000); // 5 Minute Inaktivität
time = setTimeout(logout, 4 * 60 * 60 * 1000);
}
};
inactivityTime();
""";
UI.getCurrent().getPage().executeJs(jsCode, this);
}
var horizontalLayout = new HorizontalLayout();
add(horizontalLayout);
private void setupWelcomeMessage(Div container) {
welcomeMessage.addClassName("welcome-panel");
welcomeMessage.getStyle()
.set("display", "flex")
.set("flex-direction", "column")
.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");
Button startBtn = new Button("Start", event -> startup());
Icon icon = new Icon(VaadinIcon.DESKTOP);
icon.setSize("48px");
icon.getStyle().set("margin-bottom", "12px");
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");
Paragraph description = new Paragraph("Klicken Sie auf 'Start', um Ihren Android Emulator zu starten.");
description.getStyle().set("margin", "0").set("text-align", "center").set("font-size", "0.9rem");
welcomeMessage.add(icon, title, description);
container.add(welcomeMessage);
}
private void setupButtonLayout(Div container) {
var buttonLayout = new HorizontalLayout();
buttonLayout.setJustifyContentMode(HorizontalLayout.JustifyContentMode.CENTER);
buttonLayout.getStyle()
.set("margin", "0")
.set("padding", "8px 0")
.set("flex-shrink", "0");
Button startBtn = new Button("Emulator starten", new Icon(VaadinIcon.PLAY), event -> startup());
startBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
horizontalLayout.add(startBtn);
Button logoutBtn = new Button("Logout", event -> logout());
logoutBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
horizontalLayout.add(logoutBtn);
Button logoutBtn = new Button("Abmelden", new Icon(VaadinIcon.SIGN_OUT), event -> logout());
logoutBtn.addThemeVariants(ButtonVariant.LUMO_CONTRAST);
webView = new IFrame();
webView.setSizeFull(); // IFrame auf volle Größe setzen
webView.getElement().setAttribute("frameborder", "0"); // Optional: Rahmen entfernen
buttonLayout.add(startBtn, logoutBtn);
container.add(buttonLayout);
}
// Layout konfigurieren
setSizeFull(); // Vertikales Layout auf volle Größe setzen
add(webView);
private void setupEmulatorContainer(Div container) {
emulatorContainer.getStyle()
.set("border", "2px solid #e9ecef")
.set("border-radius", "8px")
.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");
addAttachListener(event -> {
event.getUI();
});
webView.setSizeFull();
webView.getElement().setAttribute("frameborder", "0");
emulatorContainer.add(webView);
container.add(emulatorContainer);
}
private void loadSessionData() {
var currentUI = getUI();
if (currentUI.isPresent()) {
var vaadinSession = currentUI.get().getSession();
username = (String) vaadinSession.getAttribute("username");
niederlassung = (String) vaadinSession.getAttribute("niederlassung");
server = (String) vaadinSession.getAttribute("server");
} else {
System.err.println("UI nicht verfügbar - kann Session-Daten nicht laden");
}
if (server == null) {
server = "172.16.0.158";
}
System.out.println("MainView Session-Daten geladen:");
System.out.println("Username: " + username);
System.out.println("Niederlassung: " + niederlassung);
System.out.println("Server: " + server);
if (niederlassung == null) {
System.err.println("FEHLER: Niederlassung ist null! Benutzer muss sich neu anmelden.");
getUI().ifPresent(ui -> ui.navigate("login"));
}
}
@Override
@@ -131,6 +221,10 @@ public final class MainView extends Main implements BeforeEnterObserver
ui.access(() -> {
dialog.close();
// Willkommensnachricht ausblenden und Emulator anzeigen
welcomeMessage.getStyle().set("display", "none");
emulatorContainer.getStyle().set("display", "block");
refreshWebView();
});
}
@@ -144,7 +238,18 @@ public final class MainView extends Main implements BeforeEnterObserver
}
private void refreshWebView() {
var url = "https://sb-app.emu.assecutor.org" + Application.users.get(username).urlExtension() + "?autoconnect=true";
if (niederlassung == null) {
System.err.println("Niederlassung ist null - kann WebView nicht aktualisieren");
return;
}
var niederlassungInfo = Application.niederlassungen.get(niederlassung);
if (niederlassungInfo == null) {
System.err.println("Niederlassungsinfo für '" + niederlassung + "' nicht gefunden");
return;
}
var url = "https://sb-app.emu.assecutor.org" + niederlassungInfo.urlExtension() + "?autoconnect=true";
System.out.println("URL: " + url);
@@ -181,10 +286,10 @@ public final class MainView extends Main implements BeforeEnterObserver
exec(execId);
} catch (Exception e) {
e.printStackTrace();
logger.error("Fehler beim Senden der HTTP-Anfrage für App-Download", e);
}
} catch (Exception e) {
e.printStackTrace();
logger.error("Fehler beim Download der App", e);
}
}
@@ -213,7 +318,7 @@ public final class MainView extends Main implements BeforeEnterObserver
exec(execId);
} catch (Exception e) {
e.printStackTrace();
logger.error("Fehler bei der App-Installation", e);
}
}
@@ -233,7 +338,7 @@ public final class MainView extends Main implements BeforeEnterObserver
return response.body();
} catch (Exception e) {
e.printStackTrace(); // Fehler behandeln
logger.error("Fehler beim Ausführen des Befehls mit execId: {}", execId, e);
}
return null;
@@ -252,7 +357,7 @@ public final class MainView extends Main implements BeforeEnterObserver
System.out.println("HTTP-Response-Code: " + response.statusCode()); // Statuscode ausgeben
System.out.println("Response-Body: " + response.body()); // Antwort-Body ausgeben
} catch (Exception e) {
e.printStackTrace(); // Fehler behandeln
logger.error("Fehler beim HTTP-Request", e);
}
}
@@ -294,7 +399,7 @@ public final class MainView extends Main implements BeforeEnterObserver
}
} catch (Exception e) {
e.printStackTrace();
logger.error("Fehler beim Warten auf Container-Start", e);
}
} while (!endLoop);
}
@@ -312,6 +417,8 @@ public final class MainView extends Main implements BeforeEnterObserver
],
"HostConfig": {
"NetworkMode": "votianBridge",
"Memory": 10737418240,
"CpuCount": 4,
"Dns": [
"172.18.0.15"
],
@@ -343,7 +450,20 @@ public final class MainView extends Main implements BeforeEnterObserver
}
}
}
""".formatted(Application.users.get(username).port(), Application.users.get(username).ip());
""";
if (niederlassung == null) {
System.err.println("Niederlassung ist null - kann Container nicht erstellen");
return;
}
var niederlassungInfo = Application.niederlassungen.get(niederlassung);
if (niederlassungInfo == null) {
System.err.println("Niederlassungsinfo für '" + niederlassung + "' nicht gefunden");
return;
}
jsonPayload = jsonPayload.formatted(niederlassungInfo.port(), niederlassungInfo.ip());
System.out.println(jsonPayload);
@@ -360,7 +480,7 @@ public final class MainView extends Main implements BeforeEnterObserver
System.out.println("HTTP-Response-Code: " + response.statusCode());
System.out.println("Response-Body: " + response.body());
} catch (Exception e) {
e.printStackTrace();
logger.error("Fehler beim Erstellen des Containers", e);
}
}
@@ -377,7 +497,7 @@ public final class MainView extends Main implements BeforeEnterObserver
System.out.println("HTTP-Response-Code: " + response.statusCode()); // Statuscode ausgeben
System.out.println("Response-Body: " + response.body()); // Antwort-Body ausgeben
} catch (Exception e) {
e.printStackTrace(); // Fehler behandeln
logger.error("Fehler beim HTTP-Request", e);
}
}
@@ -394,7 +514,7 @@ public final class MainView extends Main implements BeforeEnterObserver
System.out.println("HTTP-Response-Code: " + response.statusCode()); // Statuscode ausgeben
System.out.println("Response-Body: " + response.body()); // Antwort-Body ausgeben
} catch (Exception e) {
e.printStackTrace(); // Fehler behandeln
logger.error("Fehler beim HTTP-Request", e);
}
}
@@ -432,10 +552,15 @@ public final class MainView extends Main implements BeforeEnterObserver
ui.access(() -> {
dialog.close();
// Session cleanup vor Navigation
if (niederlassung != null) {
Application.activeNiederlassungen.remove(niederlassung);
}
vaadinSession.invalidate();
// Navigiere den Benutzer zur Main-Seite
ui.navigate("main");
// Navigiere direkt zur Login-Seite
ui.navigate("login");
});
}
});

View File

@@ -2,15 +2,8 @@ package de.assecutor.emulatorstation.base.ui.view.security;
import com.vaadin.flow.spring.security.VaadinWebSecurity;
import de.assecutor.emulatorstation.base.ui.view.LoginView;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import de.assecutor.emulatorstation.Application;
@@ -21,50 +14,21 @@ public class SecurityConfig extends VaadinWebSecurity {
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
// FormLogin verwenden; Vaadin-LoginView
setLoginView(http, LoginView.class);
http
.formLogin(form -> form
.loginPage("/login")
.successHandler(authenticationSuccessHandler())
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/main")
);
}
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
Application.users.forEach((username, info) ->
manager.createUser(User.withUsername(username)
.password(info.password())
.roles("USER")
.build())
);
return manager;
}
@Bean
@SuppressWarnings("deprecation")
public PasswordEncoder passwordEncoder() {
// Für Demo-Zwecke: Passwörter sind im Klartext hinterlegt
return NoOpPasswordEncoder.getInstance();
}
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return (request, response, authentication) -> {
var session = request.getSession(true);
.logoutSuccessUrl("/login")
.addLogoutHandler((request, response, authentication) -> {
if (authentication != null) {
String username = authentication.getName();
session.setAttribute("user", username);
session.setAttribute("username", username);
String target = "admin".equalsIgnoreCase(username) ? "/admin" : "/main";
response.sendRedirect(target);
};
Application.activeNiederlassungen.entrySet().removeIf(entry ->
entry.getValue().equals(username));
}
})
);
}
}

View File

@@ -0,0 +1,20 @@
package de.assecutor.emulatorstation.base.ui.view.security;
import com.vaadin.flow.server.SessionDestroyEvent;
import com.vaadin.flow.server.SessionDestroyListener;
import com.vaadin.flow.spring.annotation.SpringComponent;
import de.assecutor.emulatorstation.Application;
@SpringComponent
public class SessionListener implements SessionDestroyListener {
@Override
public void sessionDestroy(SessionDestroyEvent event) {
String username = (String) event.getSession().getAttribute("user");
String niederlassung = (String) event.getSession().getAttribute("niederlassung");
if (username != null && niederlassung != null) {
Application.activeNiederlassungen.remove(niederlassung);
}
}
}

View File

@@ -0,0 +1,4 @@
package de.assecutor.emulatorstation.pojo;
public record NiederlassungInfo(String name, String ip, String port, String urlExtension) {
}

View File

@@ -1,6 +1,6 @@
package de.assecutor.emulatorstation.pojo;
public record UserInfo(String password, String ip, String port, String urlExtension) {
public record UserInfo(String password) {
}