Show SIM occupancy badge and add release action
- SimCardConfigurationView: render dropdown rows with a "Belegt durch <SID>"
badge for SIM cards that have a usr_id, plus an inline unlink button that
opens a confirmation dialog and releases the assignment.
- SimCardAssignmentService: add releaseSimCard via DELETE
/api/simcards/{id}/assign.
- MainView: after install, auto-start the installed app by resolving the
third-party package via "pm list packages -3" and launching it through
monkey.
- SecurityConfig: route unauthenticated users to SimCardConfigurationView
when SSO is disabled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,21 @@ public class SimCardAssignmentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
public void assignSimCard(CourierInfo courier, SimCardInfo simCard) {
|
||||||
try {
|
try {
|
||||||
String payload = objectMapper.writeValueAsString(
|
String payload = objectMapper.writeValueAsString(
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ 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;
|
||||||
|
|
||||||
@AnonymousAllowed
|
@AnonymousAllowed
|
||||||
@Route("main")
|
@Route("main")
|
||||||
@@ -335,6 +337,8 @@ public final class MainView extends Main implements BeforeEnterObserver {
|
|||||||
downloadApp();
|
downloadApp();
|
||||||
|
|
||||||
installApp();
|
installApp();
|
||||||
|
|
||||||
|
startApp();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
// Logging, Fehlerbehandlung …
|
// Logging, Fehlerbehandlung …
|
||||||
@@ -415,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();
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ 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.H1;
|
import com.vaadin.flow.component.html.H1;
|
||||||
import com.vaadin.flow.component.html.Paragraph;
|
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.Icon;
|
||||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||||
import com.vaadin.flow.component.notification.Notification;
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
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.BeforeEnterEvent;
|
||||||
import com.vaadin.flow.router.BeforeEnterObserver;
|
import com.vaadin.flow.router.BeforeEnterObserver;
|
||||||
import com.vaadin.flow.router.PageTitle;
|
import com.vaadin.flow.router.PageTitle;
|
||||||
@@ -28,14 +30,20 @@ import de.assecutor.emulatorstation.base.ui.view.security.SsoConfiguration;
|
|||||||
import de.assecutor.emulatorstation.pojo.CourierInfo;
|
import de.assecutor.emulatorstation.pojo.CourierInfo;
|
||||||
import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
|
import de.assecutor.emulatorstation.pojo.NiederlassungInfo;
|
||||||
import de.assecutor.emulatorstation.pojo.SimCardInfo;
|
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.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Route("sim-config")
|
@Route("sim-config")
|
||||||
@PageTitle("SIM-Konfiguration")
|
@PageTitle("SIM-Konfiguration")
|
||||||
@AnonymousAllowed
|
@AnonymousAllowed
|
||||||
public class SimCardConfigurationView extends VerticalLayout implements BeforeEnterObserver {
|
public class SimCardConfigurationView extends VerticalLayout implements BeforeEnterObserver {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(SimCardConfigurationView.class);
|
||||||
|
|
||||||
private final AuthenticationContext authenticationContext;
|
private final AuthenticationContext authenticationContext;
|
||||||
private final LoginSessionService loginSessionService;
|
private final LoginSessionService loginSessionService;
|
||||||
private final EmulatorContainerService emulatorContainerService;
|
private final EmulatorContainerService emulatorContainerService;
|
||||||
@@ -47,6 +55,7 @@ public class SimCardConfigurationView extends VerticalLayout implements BeforeEn
|
|||||||
private final ComboBox<SimCardInfo> simCardSelect = new ComboBox<>("SIM-Karte");
|
private final ComboBox<SimCardInfo> simCardSelect = new ComboBox<>("SIM-Karte");
|
||||||
private final Paragraph statusText = new Paragraph("Lade Kuriere und SIM-Karten...");
|
private final Paragraph statusText = new Paragraph("Lade Kuriere und SIM-Karten...");
|
||||||
private final Button assignButton;
|
private final Button assignButton;
|
||||||
|
private final Map<Long, String> courierSidByUserId = new HashMap<>();
|
||||||
|
|
||||||
public SimCardConfigurationView(AuthenticationContext authenticationContext,
|
public SimCardConfigurationView(AuthenticationContext authenticationContext,
|
||||||
LoginSessionService loginSessionService, EmulatorContainerService emulatorContainerService,
|
LoginSessionService loginSessionService, EmulatorContainerService emulatorContainerService,
|
||||||
@@ -89,6 +98,39 @@ public class SimCardConfigurationView extends VerticalLayout implements BeforeEn
|
|||||||
simCardSelect.setWidthFull();
|
simCardSelect.setWidthFull();
|
||||||
simCardSelect.setPlaceholder("SIM-Karte auswählen");
|
simCardSelect.setPlaceholder("SIM-Karte auswählen");
|
||||||
simCardSelect.setItemLabelGenerator(SimCardInfo::displayLabel);
|
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 = new Button("Weiter", new Icon(VaadinIcon.CHECK), event -> assignSimCard());
|
||||||
assignButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
assignButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
@@ -135,6 +177,11 @@ public class SimCardConfigurationView extends VerticalLayout implements BeforeEn
|
|||||||
List<CourierInfo> couriers = simCardAssignmentService.fetchCouriers();
|
List<CourierInfo> couriers = simCardAssignmentService.fetchCouriers();
|
||||||
List<SimCardInfo> simCards = simCardAssignmentService.fetchSimCards();
|
List<SimCardInfo> simCards = simCardAssignmentService.fetchSimCards();
|
||||||
|
|
||||||
|
courierSidByUserId.clear();
|
||||||
|
for (CourierInfo courier : couriers) {
|
||||||
|
courierSidByUserId.putIfAbsent(courier.userId(), courier.courierSid());
|
||||||
|
}
|
||||||
|
|
||||||
courierSelect.setItems(couriers);
|
courierSelect.setItems(couriers);
|
||||||
simCardSelect.setItems(simCards);
|
simCardSelect.setItems(simCards);
|
||||||
courierSelect.clear();
|
courierSelect.clear();
|
||||||
@@ -217,6 +264,8 @@ public class SimCardConfigurationView extends VerticalLayout implements BeforeEn
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
logger.info("Sende SIM-Zuordnung: Kurier='{}', SIM-Karte='{}'", courier.displayLabel(),
|
||||||
|
simCard.displayLabel());
|
||||||
simCardAssignmentService.assignSimCard(courier, simCard);
|
simCardAssignmentService.assignSimCard(courier, simCard);
|
||||||
loginSessionService.markSimConfigurationCompleted(ui.getSession());
|
loginSessionService.markSimConfigurationCompleted(ui.getSession());
|
||||||
statusText.setText("SIM-Karte wurde erfolgreich zugeordnet.");
|
statusText.setText("SIM-Karte wurde erfolgreich zugeordnet.");
|
||||||
@@ -233,4 +282,45 @@ public class SimCardConfigurationView extends VerticalLayout implements BeforeEn
|
|||||||
statusText.setText(message);
|
statusText.setText(message);
|
||||||
Notification.show(message, 5000, Notification.Position.MIDDLE);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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.authentication.logout.LogoutSuccessHandler;
|
||||||
@@ -24,7 +25,11 @@ public class SecurityConfig extends VaadinWebSecurity {
|
|||||||
protected void configure(HttpSecurity http) throws Exception {
|
protected void configure(HttpSecurity http) throws Exception {
|
||||||
super.configure(http);
|
super.configure(http);
|
||||||
|
|
||||||
setLoginView(http, LoginView.class);
|
if (ssoConfiguration.isEnabled()) {
|
||||||
|
setLoginView(http, LoginView.class);
|
||||||
|
} else {
|
||||||
|
setLoginView(http, SimCardConfigurationView.class);
|
||||||
|
}
|
||||||
|
|
||||||
http.sessionManagement(session -> session
|
http.sessionManagement(session -> session
|
||||||
// Wichtig: Session-Fixation so konfigurieren, dass Session-Attribute (z.B. "user") erhalten bleiben
|
// Wichtig: Session-Fixation so konfigurieren, dass Session-Attribute (z.B. "user") erhalten bleiben
|
||||||
|
|||||||
Reference in New Issue
Block a user