Adventskalender 2025 – Minimaler Login Prozess – Teil 1

Sven Ruppert

Das Administrations-Interface eines URL-Shorteners stellt einen sensiblen Bereich dar, in dem Kurzlinks geändert, entfernt oder mit Ablaufdaten versehen werden können. Obwohl das System in vielen Fällen auf einem internen Server oder im privaten Umfeld betrieben wird, bleibt der Schutz dieser Verwaltungsoberfläche ein grundlegender Sicherheitsfaktor. Ein versehentlicher Zugriff durch Unbefugte kann nicht nur zu falschen Weiterleitungen oder Datenverlust führen, sondern auch das Vertrauen in das Gesamtsystem beeinträchtigen.

Mit der Einführung eines konfigurierbaren Login-Mechanismus wird erstmals eine klare Zugriffskontrolle geschaffen, die den Zugriff auf die Management-Funktionen bewusst vom restlichen System trennt. Der Login dient als leichtgewichtige Sicherheitsmaßnahme, die ohne externe Abhängigkeiten, ohne Frameworks und ohne aufwendige Benutzerverwaltung auskommt. Genau diese Einfachheit – ein einzelnes Passwort, eine einfache Konfigurationsdatei und ein zentrierter Login-Screen – macht die Lösung für kleine Deployments oder persönliche Projekte besonders attraktiv.

Der Quelltext zu diesem Artikel befindet sich auf GitHub unter der folgenden URL: https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-09

Ziele der Implementierung

Die Einführung eines einfachen, konfigurierbaren Login-Mechanismus verfolgt mehrere klar voneinander abgegrenzte Ziele, die sowohl die Sicherheit als auch die Bedienbarkeit des URL-Shorteners verbessern. Dabei steht nicht die Etablierung eines komplexen Benutzer- und Rollenmodells im Vordergrund, sondern die Schaffung einer pragmatischen Schutzschicht, die den administrativen Zugriff zuverlässig kontrolliert, ohne den Entwicklungs- oder Betriebsaufwand unnötig zu erhöhen.

Zentral ist zunächst die Absicherung der administrativen Funktionen. Die Verwaltungsoberfläche ermöglicht das Erstellen, Bearbeiten und Entfernen von Kurzlinks sowie das Setzen oder Bereinigen von Ablaufdaten. Diese Funktionen müssen vor unbefugtem Zugriff geschützt werden, insbesondere wenn der URL-Shortener nicht ausschließlich im geschlossenen Netzwerk betrieben wird. Ein einfacher Login verhindert, dass versehentliche oder neugierige Zugriffe Schaden verursachen.

Ein weiteres Ziel ist die Minimierung der Einstiegshürde. Die Lösung soll ohne zusätzliche Frameworks, externe Identitätsprovider und komplexe Konfiguration auskommen. Die Implementierung orientiert sich bewusst an der Philosophie des Projekts: Alles bleibt überschaubar, leichtgewichtig und verständlich. Mit einer einfachen auth.properties-Datei lässt sich das System flexibel aktivieren oder deaktivieren, und ein einziges Passwort genügt für die Zugriffskontrolle.

Darüber hinaus soll der Login-Mechanismus ein vorhersehbares und konsistentes Benutzererlebnis gewährleisten. Dazu gehören eine klar gestaltete Login-Seite, ein sofortiger Redirect bei nicht authentifizierten Zugriffen auf geschützte Bereiche sowie ein transparenter Logout-Prozess, der die Sitzung sauber beendet. All diese Aspekte sorgen dafür, dass sich der Login natürlich in die bestehende UI einfügt und dennoch als Sicherheitsmaßnahme eindeutig wahrgenommen wird.

Schließlich dient der Login auch als Grundlage für mögliche spätere Erweiterungen. Obwohl die aktuelle Implementierung bewusst minimalistisch ist, schafft sie die strukturellen Voraussetzungen, um bei Bedarf auf stärkere Sicherheitsanforderungen reagieren zu können — etwa durch Erweiterungen um Hashing-Mechanismen, zeitlich begrenzte Session-Tokens oder eine serverseitige Passwortrotation. Damit bleibt das System offen für Weiterentwicklung, ohne dabei seine derzeitige Einfachheit zu verlieren.

Konfigurierbarer Login über auth.properties

Die Grundlage des neuen Login-Mechanismus ist eine bewusst schlicht gehaltene Konfigurationsdatei, die es ermöglicht, das gesamte Authentifizierungsverhalten der Anwendung zentral zu steuern. Diese Datei trägt den Namen auth.properties und befindet sich im Ressourcenverzeichnis der Anwendung. Durch ihren Einsatz lässt sich das Login-System nicht nur bequem aktivieren oder deaktivieren, sondern auch schnell an unterschiedliche Einsatzszenarien anpassen.

Im Mittelpunkt stehen zwei Konfigurationsschlüssel: login.enabled und login.password. Während der Erste prüft, ob das Login-System überhaupt aktiv ist, legt der zweite Parameter das erforderliche Zugangspasswort fest. Diese Werte werden beim Start der Anwendung automatisch eingelesen und bestimmen fortan das Verhalten sämtlicher geschützten Bereiche. Gerade dieser Mechanismus ermöglicht es, das Login kurzfristig abzuschalten oder das Passwort ohne erneute Anpassung im Quellcode zu ändern.

Das Einlesen der Konfiguration erfolgt über den LoginConfigInitializer, der beim Start des Servlet-Containers ausgeführt wird. Er prüft, ob die Datei vorhanden ist, lädt deren Inhalte und übergibt sie der zentralen Klasse LoginConfig, die diese Werte verwaltet und für spätere Zugriffe bereitstellt.

Diese Klasse übernimmt die vollständige Verantwortung dafür, das Login-Verhalten der Anwendung beim Start korrekt vorzubereiten. Dadurch ist gewährleistet, dass sowohl die Login-Seite als auch der Route-Schutz jederzeit auf eine einheitliche Konfigurationsbasis zugreifen können.

Nach dem Laden der Datei übernimmt die Klasse LoginConfig die Aufgabe, die Werte dauerhaft bereitzustellen und Passwortvergleiche durchzuführen.

Diese Form der Konfigurationssteuerung vereinfacht nicht nur die Bedienung, sondern unterstützt auch verschiedene Betriebsmodi. Ein vollständig deaktiviertes Login-System eignet sich beispielsweise für rein interne Entwicklungsinstallationen, während der aktivierte Modus in produktiven oder öffentlich zugänglichen Szenarien Schutz vor unbefugten Änderungen bietet. Der Wechsel zwischen beiden Modi ist dabei so niedrigschwellig wie möglich gehalten – ein einfacher Wert in einer Textdatei genügt.

Obwohl dieser Mechanismus grundlegende Schutzfunktionen bietet, ist er ausdrücklich nicht als hochsichere Authentifizierungslösung konzipiert. Das Ziel besteht vielmehr darin, eine minimale Absicherung zu schaffen, die den administrativen Bereich vor zufälligen Zugriffen oder unbefugter Nutzung schützt. Für produktive Umgebungen mit erhöhten Sicherheitsanforderungen wären zusätzliche Maßnahmen wie Passwort-Hashing, Multi-Faktor-Authentifizierung oder die Integration in etablierte Identitätsdienste notwendig.

LoginConfig & Initialisierung

Die zentrale Steuerung des Login-Verhaltens basiert auf zwei eng verzahnten Komponenten: der Klasse LoginConfig selbst und ihrem vorgelagerten Initialisierer LoginConfigInitializer. Gemeinsam sorgen sie dafür, dass die Konfiguration aus der Datei auth.properties korrekt eingelesen, interpretiert und für die Laufzeit der Anwendung verfügbar gemacht wird.

Im Mittelpunkt steht zunächst die Klasse LoginConfig. Sie stellt die minimalistische, aber schlüssige Grundlage für das Login-System bereit. Der Ansatz ist absichtlich einfach gehalten: Es gibt keinen Benutzerstamm, keine Rollen oder Profile, sondern lediglich ein einziges Passwort, das als Zugangsschwelle dient. Die Klasse verwaltet dieses Passwort sowie die Information darüber, ob der Login überhaupt aktiv sein soll. Der Aufbau bleibt überschaubar, um die Einstiegshürde für Administratoren und Entwickler gering zu halten.

Ein wesentliches Detail ist dabei die Aufteilung in zwei Phasen: Zuerst wird beim Start der Anwendung im LoginConfigInitializer geprüft, welche Einstellungen in der Konfigurationsdatei hinterlegt sind. Anschließend werden diese Werte an die statische Methode LoginConfig.initialise() übergeben, die sie in die entsprechenden Felder übernimmt und damit für den Rest der Anwendung wirksam macht.

Dieser Initialisierungsvorgang erfolgt vollständig beim Start des Servlet-Containers. Dadurch ist gewährleistet, dass alle später geladenen Views, insbesondere der Route-Schutz und die Login-Seite, Zugriff auf eine konsistente und vollständig initialisierte Konfiguration haben. Das vermeidet Fehlerzustände, die etwa durch fehlende oder verspätet geladene Konfigurationswerte entstehen könnten.

Im Folgenden betrachten wir beide Komponenten etwas genauer und verweisen auf die Originalquelltexte zur besseren Nachvollziehbarkeit.

LoginConfig – zentrale Konfigurationsklasse

Den Kern bildet die Klasse LoginConfig, die den Schalter für das Login sowie das erwartete Passwort als Byte-Daten hält. Sie ist als final deklariert und verfügt über einen privaten Konstruktor, sodass sie ausschließlich über ihre statischen Methoden verwendet wird:

/**
 * Central configuration for the simple admin login.
 * Reads its values from auth.properties via LoginConfigInitializer.
 */
public final class LoginConfig {

  private static volatile boolean loginEnabled;
  private static volatile byte[] expectedPasswordBytes;

  private LoginConfig() {
  }

  /**
   * Initialises the login configuration.
   *
   * @param enabled  whether the login mechanism should be enforced
   * @param password the raw password read from configuration, may be {@code null}
   */
  public static void initialise(boolean enabled, String password) {
    loginEnabled = enabled;

    if (!enabled || password == null || password.isBlank()) {
      expectedPasswordBytes = null;
      return;
    }

    expectedPasswordBytes = password.getBytes(StandardCharsets.UTF_8);
  }

  /**
   * @return {@code true} if login protection is enabled at all
   */
  public static boolean isLoginEnabled() {
    return loginEnabled;
  }

  /**
   * @return {@code true} if login is enabled and a usable password has been configured
   */
  public static boolean isLoginConfigured() {
    return loginEnabled
        && expectedPasswordBytes != null
        && expectedPasswordBytes.length > 0;
  }

  /**
   * Compares the entered password with the configured one using constant-time comparison.
   */
  public static boolean matches(char[] enteredPassword) {
    if (!isLoginConfigured() || enteredPassword == null) {
      return false;
    }

    byte[] entered = new String(enteredPassword).getBytes(StandardCharsets.UTF_8);
    boolean result = MessageDigest.isEqual(expectedPasswordBytes, entered);

    // Best-effort clean-up
    Arrays.fill(entered, (byte) 0);
    return result;
  }
}

Die Methode initialise wird ausschließlich beim Start des Initialisierers aufgerufen und bestimmt, ob der Login aktiviert ist (loginEnabled) und ob ein gültiges Passwort vorliegt. Dabei werden ungültige oder leere Passwörter konsequent verworfen, sodass isLoginConfigured() nur dann true zurückliefert, wenn tatsächlich eine verwendbare Konfiguration vorliegt.

Für den eigentlichen Passwortvergleich stellt matches(char[] enteredPassword) eine zentrale Funktion bereit. Sie nimmt das eingegebene Passwort als char[] entgegen, wandelt es in ein Byte-Array im UTF-8-Format um und vergleicht dieses mittels MessageDigest.isEqual mit der erwarteten Byte-Sequenz. Damit wird ein konstanter Zeitvergleich erreicht, der einfache Timing-Angriffe erschwert. Anschließend wird das temporäre Byte-Array mit Arrays.fill überschrieben, um die sensiblen Daten zumindest bestmöglich aus dem Speicher zu entfernen.

LoginConfigInitializer – Laden der Konfiguration beim Start

Damit LoginConfig mit sinnvollen Werten arbeiten kann, muss die Konfigurationsdatei beim Start des Servlet-Containers eingelesen werden. Diese Aufgabe übernimmt die Klasse LoginConfigInitializer, die als @WebListener registriert ist und das Interface ServletContextListener implementiert:

@WebListener
public class LoginConfigInitializer implements ServletContextListener, HasLogger {

  private static final String PROPERTIES_PATH = "auth.properties";

  @Override
  public void contextInitialized(ServletContextEvent sce) {
    logger().info("Initialising LoginConfig from {}", PROPERTIES_PATH);

    Properties props = new Properties();

    try (InputStream in = getClass()
        .getClassLoader()
        .getResourceAsStream(PROPERTIES_PATH)) {

      if (in == null) {
        logger().warn("No {} found on classpath. Login will be disabled.", PROPERTIES_PATH);
        LoginConfig.initialise(false, null);
        return;
      }

      props.load(in);

      String enabledRaw = props.getProperty("login.enabled", "true").trim();
      boolean enabled = Boolean.parseBoolean(enabledRaw);
      String password = props.getProperty("login.password");

      LoginConfig.initialise(enabled, password);

      if (!enabled) {
        logger().info("Login explicitly disabled via login.enabled=false");
      } else if (LoginConfig.isLoginConfigured()) {
        logger().info("LoginConfig initialised successfully from {}", PROPERTIES_PATH);
      } else {
        logger().warn("login.enabled=true but no usable password configured. "
                          + "Login will effectively be disabled.");
      }

    } catch (IOException e) {
      logger().error("Failed to load " + PROPERTIES_PATH + ". Login will be disabled.", e);
      LoginConfig.initialise(false, null);
    }
  }

  @Override
  public void contextDestroyed(ServletContextEvent sce) {
    // Nothing to clean up
  }
}

Der Initialisierer folgt einem klaren Ablauf: Zunächst wird versucht, die Datei auth.properties aus dem Klassenpfad zu laden. Ist dies nicht möglich, wird der Login explizit deaktiviert und ein Warnlog wird geschrieben. Im Erfolgsfall werden die Konfigurationswerte „login.enabled“ und „login.password“ ausgelesen, in passende Datentypen überführt und an LoginConfig.initialise weitergeleitet. Abschließend erfolgen je nach Konfiguration weitere Logausgaben, die im Betrieb schnellen Aufschluss über den aktiven Modus geben.

Durch diese Trennung der Verantwortlichkeiten entsteht ein gut nachvollziehbarer Initialisierungspfad: LoginConfigInitializer kümmert sich um das Laden und Interpretieren der Konfigurationsdatei, während LoginConfig die eigentliche Login-Logik kapselt und später von anderen Komponenten wie der Login-View oder dem Route-Schutz genutzt wird.

Die neue Login-Seite

Mit der Einführung des Admin-Logins erhält die Anwendung eine eigene Einstiegsseite, die sich bewusst vom übrigen UI abgrenzt. Die neue Login-Seite bildet dabei die Schwelle zwischen öffentlichen Funktionen wie der eigentlichen Linkweiterleitung und den sensiblen Verwaltungsfunktionen im Backend. Ziel war es, ein reduziertes, klar fokussiertes Layout zu schaffen, das sich nahtlos in das visuelle Erscheinungsbild der Anwendung einfügt und dennoch als Sicherheitsbarriere eindeutig erkennbar ist.

Statt in das bestehende Navigationslayout eingebettet zu werden, wird der Login als eigenständige View ohne MainLayout gerendert. Nutzer sehen somit keine Seitennavigation, keinen Drawer und keine Admin-Funktionen, bevor sie sich erfolgreich authentifiziert haben. Die Seite füllt den gesamten Browserbereich aus; der Inhalt ist vertikal und horizontal zentriert. Dadurch entsteht der Eindruck eines klassischen Login-Screens, der nur eine Aufgabe verfolgt: das Passwort für den administrativen Bereich abzufragen.

Im Zentrum steht ein kompaktes Login-Panel, das aus einer Überschrift, einem kurzen Erläuterungstext, einem Passwortfeld und einem Login-Button besteht. Die Überschrift vermittelt klar, dass es sich um einen Admin-Zugang handelt, während der begleitende Text erläutert, warum ein Passwort erforderlich ist und welchen Bereich es schützt. Dies schafft Transparenz und reduziert Rückfragen, insbesondere wenn das System von mehreren Personen genutzt wird.

Das Passwortfeld ist so konfiguriert, dass es beim Aufruf der Seite automatisch den Fokus erhält. Anwender können also direkt tippen, ohne zunächst mit der Maus klicken zu müssen. Ergänzend sorgt ein Clear-Button dafür, dass eine versehentlich eingegebene Zeichenfolge mit einem Klick entfernt werden kann. Auf eine Anzeige des Passwortes im Klartext wird bewusst verzichtet, um das Risiko von Schulterblicken zu verringern, insbesondere in gemeinsam genutzten Arbeitsumgebungen.

Auch das Verhalten bei der Eingabe ist auf einen flüssigen Ablauf ausgelegt. Die Authentifizierung kann entweder durch einen Klick auf den Login-Button oder über die Eingabetaste erfolgen. Schlägt der Login fehl, markiert die View das Passwortfeld als ungültig und blendet eine präzise Fehlermeldung ein, die deutlich macht, dass das eingegebene Passwort nicht korrekt ist. Dadurch bleibt der Kontext erhalten und der Benutzer kann unmittelbar einen zweiten Versuch starten, ohne die Seite neu zu laden.

Implementierung der LoginView

Die technische Umsetzung der beschriebenen Login-Seite erfolgt in der Klasse LoginView. Sie ist als eigene Route registriert und verzichtet bewusst auf ein Layout, um einen fokussierten, vollflächigen Login-Screen zu ermöglichen:

@Route(LoginView.PATH) // No layout = no navigation or drawer visible
@PageTitle("Admin Login | URL Shortener")
public class LoginView
    extends VerticalLayout
    implements BeforeEnterObserver {

  public static final String PATH = "login";

  private final PasswordField passwordField = new PasswordField("Password");
  private final Button loginButton = new Button("Login");

  public LoginView() {
    setSizeFull();
    setAlignItems(Alignment.CENTER);
    setJustifyContentMode(JustifyContentMode.CENTER);

    configureForm();
    buildLayout();
  }

Bereits an der Deklaration fällt auf, dass LoginView nicht im MainLayout eingebettet ist. Durch die Route-Annotation ohne Layout wird die Seite isoliert dargestellt; die vollständige Vertikal- und Horizontalzentrierung erfolgt über die Layout-Eigenschaften im Konstruktor. Die beiden zentralen UI-Komponenten – Passwortfeld und Login-Button – werden als Felder gehalten, sodass sie in den Hilfsmethoden configureForm() und buildLayout() weiter konfiguriert werden können.

Die Konfiguration des Formulars ist bewusst auf ein schlankes, aber flüssiges Benutzererlebnis ausgelegt:

  private void configureForm() {
    passwordField.setAutofocus(true);
    passwordField.setWidth("300px");
    passwordField.setClearButtonVisible(true);
    passwordField.setRevealButtonVisible(false);
    passwordField.setInvalid(false);

    loginButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
    loginButton.setWidth("300px");
    loginButton.addClickListener(_ -> attemptLogin());

    passwordField.addKeyDownListener(event -> {
      if ("Enter".equalsIgnoreCase(event.getKey().getKeys().getFirst())) {
        attemptLogin();
      }
    });

    passwordField.addValueChangeListener(_ -> passwordField.setInvalid(false));
  }

Das Passwortfeld erhält beim Laden der Seite automatisch den Fokus und wird auf eine feste Breite gebracht, sodass Eingabe und Button visuell eine Einheit bilden. Der Clear-Button ermöglicht das schnelle Löschen einer Fehleingabe; die Anzeige des Passworts im Klartext ist explizit deaktiviert. Der Login-Button ist als primärer Button gekennzeichnet und löst, ebenso wie das Drücken der Enter-Taste, die Methode attemptLogin() aus. Über den Value-Change-Listener wird sichergestellt, dass ein zuvor gesetzter Fehlerzustand beim erneuten Tippen wiederhergestellt wird.

Die Struktur des Panels, das in der Mitte des Bildschirms angezeigt wird, wird in buildLayout() definiert:

  private void buildLayout() {
    H2 title = new H2("Admin Login");
    Paragraph subtitle = new Paragraph(
        "Please enter the administrator password to access the management interface."
    );

    VerticalLayout formLayout = new VerticalLayout(title, subtitle, passwordField, loginButton);
    formLayout.setSpacing(true);
    formLayout.setPadding(true);
    formLayout.setAlignItems(Alignment.CENTER);

    add(formLayout);
  }

Die Kombination aus Überschrift, erläuterndem Absatz, Passwortfeld und Button bildet genau das im vorigen Abschnitt beschriebene kompakte Login-Panel. Durch die Zentrierung im inneren VerticalLayout entsteht ein klarer Fokus auf den Eingabebereich, ohne visuelle Ablenkungen durch weitere UI-Elemente.

Der eigentliche Login-Vorgang wird in attemptLogin() gekapselt. Hier werden Konfiguration und Benutzereingabe zusammengeführt:

  private void attemptLogin() {
    if (!LoginConfig.isLoginEnabled()) {
      Notification.show(
          "Login is currently disabled. Please check the server configuration."
          3000,
          Notification.Position.MIDDLE
      );
      UI.getCurrent().navigate(OverviewView.PATH);
      return;
    }

    char[] input = passwordField.getValue() != null
        ? passwordField.getValue().toCharArray()
        : new char[0];

    if (!LoginConfig.isLoginConfigured()) {
      Notification.show(
          "Login is not configured. Please verify that the configuration file has been loaded.",
          3000,
          Notification.Position.MIDDLE
      );
      return;
    }

    boolean authenticated = LoginConfig.matches(input);

    if (authenticated) {
      SessionAuth.markAuthenticated();
      UI.getCurrent().navigate(OverviewView.PATH);
    } else {
      passwordField.setErrorMessage("Incorrect password");
      passwordField.setInvalid(true);
    }
  }

Zunächst wird geprüft, ob der Login laut Konfiguration überhaupt aktiviert ist. Ist dies nicht der Fall, erhält der Benutzer eine klare Rückmeldung per Notification und wird direkt zur Übersichtsseite weitergeleitet. Im nächsten Schritt wird geprüft, ob der Login korrekt konfiguriert wurde. Erst wenn beide Bedingungen erfüllt sind, wird das eingegebene Passwort an LoginConfig.matches übergeben. Bei Erfolg wird die Sitzung über SessionAuth.markAuthenticated() als authentifiziert markiert und zur Overview weitergeleitet. Im Fehlerfall markiert die View das Passwortfeld als ungültig und zeigt eine eindeutige Fehlermeldung an – der Benutzer bleibt auf der Login-Seite und kann die Eingabe anpassen.

Schließlich sorgt die Implementierung des BeforeEnterObserver dafür, dass die Login-Seite selbst nur dann sichtbar bleibt, wenn es sinnvoll ist:

  @Override
  public void beforeEnter(BeforeEnterEvent event) {
    // If login is disabled, skip the login page entirely
    if (!LoginConfig.isLoginEnabled()) {
      event.forwardTo(OverviewView.PATH);
      return;
    }

    // If already authenticated, also skip the login page
    if (SessionAuth.isAuthenticated()) {
      event.forwardTo(OverviewView.PATH);
    }
  }

Damit wird verhindert, dass bereits authentifizierte Nutzer erneut auf der Login-Seite landen, und gleichzeitig sichergestellt, dass ein global deaktiviertes Login-System die Benutzer nicht unnötig mit einem überflüssigen Passwortdialog konfrontiert.

Ein weiterer Aspekt ist das Zusammenspiel mit der Konfiguration. Ist der Login in der auth.properties-Datei deaktiviert oder nicht sinnvoll konfiguriert, reagiert die View mit klaren Hinweisen. In einem Fall informiert sie darüber, dass der Login derzeit abgeschaltet ist, und leitet direkt zur Übersichtsseite weiter. Im anderen Fall weist sie darauf hin, dass keine gültige Konfiguration geladen werden konnte. Die Seite dient damit nicht nur als Eingabeformular, sondern zugleich als Diagnosepunkt, an dem Fehlkonfigurationen frühzeitig sichtbar werden.

Insgesamt sorgt die neue Login-Seite dafür, dass der Einstieg in das Admin-Interface strukturiert, vorhersehbar und visuell klar vom restlichen System abgegrenzt ist. Sie verbindet eine bewusst einfache Interaktion mit einem nachvollziehbaren Sicherheitskonzept und legt damit die Basis für die weiteren Bausteine des Login-Flows, insbesondere für den Route-Schutz und die Sitzungsverwaltung.

Cheers Sven

Total
0
Shares
Previous Post

Adventskalender 2025 – Massenoperationen im Grid – Teil 2

Next Post

Adventskalender 2025 – Minimaler Login Prozess – Teil 2

Related Posts