Spring-Boot-Adapter für die vorhandene VoTiAn-Datenbank
Java 21, Spring Boot 3.5.16 und MariaDB JDBC. Dieser Adapter verwendet ausschließlich bestehende Tabellen. Er enthält kein JPA-Modell, keine Schema-Migration, keine eingebettete Datenbank und keine Seed-Daten. Alle implementierten Datenbankabfragen sind SELECT; der Verbindungspool ist zusätzlich schreibgeschützt.
Zentrale Datenbankserviceschicht
database.DatabaseService ist die einzige Komponente mit Zugriff auf JdbcTemplate und den Transaktionsmanager. LegacyAuthenticationProvider, LegacyReadService, LegacyHistoryService und database.DatabaseDiagnostics verwenden ausschließlich diesen Service; auch Integrationstests lesen den Bestand über ihn. Der Service aktiviert setEnforceReadOnly(true), sodass jede Lesetransaktion mit SET TRANSACTION READ ONLY serverseitig abgesichert wird. Der JDBC-Hinweis allein reicht hierfür beim verwendeten MariaDB-Treiber nicht aus.
queryForList(sql, parameters...)undqueryForObject(sql, type, parameters...)führen einzelne parametrisierte SELECT-Abfragen in Lesetransaktionen aus.readTransaction(work)fasst abhängige Abfragen zusammen; Auftragsliste einschließlich Anzahl und Stationen sowie Auftragsdetails verwenden diese Grenze.checkConnection()liefert einen bereinigten Verbindungsstatus und unterscheidet Netzwerkfehler, abgewiesene Anmeldung und fehlende Datenbankrechte.update(sql, parameters...)ist der zentrale Einstieg für zukünftige Schreibprozesse. Er wirft derzeit vor jedem DatenbankzugriffDatabaseWritesDisabledException. Es wurde kein fachlicher Schreibvorgang freigeschaltet.
SQL bleibt anwendungseigener Code. Benutzereingaben dürfen nur als gebundene Werte übergeben werden. Der Service weist Nicht-SELECTs, mehrere Statements und bekannte SELECT-Schreib-/Sperrformen über die Lese-API zurück; diese Prüfung ist kein SQL-Parser für beliebige fremde Abfragen. Es werden weder JDBC-Verbindungen noch JDBC-Callbacks nach außen gegeben.
python3 scripts/check-database.py im Projektstamm startet das gebaute JAR mit --database-check. Dieser Modus nutzt denselben Service und dieselbe Poolkonfiguration ohne Webserver, gibt JSON aus und schließt den Spring-Kontext. Python stellt keine eigene Datenbankverbindung mehr her.
Start und Konfiguration
Die lokale Datenbankkonfiguration steht in der .env im Projektstamm: DB_URL, DB_USER und DB_PASSWORD. ./run.sh, ./dev.sh und python3 scripts/check-database.py laden diese Datei automatisch. Bereits gesetzte Prozessumgebungsvariablen haben Vorrang vor den Werten aus .env.
Direkte Maven-Aufrufe laden .env nicht automatisch. Für den eigenständigen Backend-Start mit der lokalen Konfiguration im Verzeichnis backend/ ausführen:
python3 ../scripts/env_config.py ./mvnw spring-boot:run
Verfügbare Konfiguration:
| Variable | Bedeutung |
|---|---|
DB_URL |
JDBC-Adresse der bestehenden DB, z. B. jdbc:mariadb://DB-HOST:3306/DB-NAME?connectTimeout=2000&socketTimeout=5000 |
DB_USER, DB_PASSWORD |
Vorhandenes Datenbankkonto; keine Zugangsdaten im Quelltext |
HISTORY_DB_SCHEMA |
Bestehendes Protokollschema auf demselben Server, Standard phoenix_log; benötigt SELECT-Rechte für log und logoperation mit demselben Datenbankkonto. Name aus Buchstaben, Ziffern und Unterstrichen, maximal 64 Zeichen |
LEGACY_APP_URL |
Optionale HTTP(S)-Basisadresse der vorhandenen PHP-Anwendung |
SERVER_ADDRESS |
Standard 127.0.0.1 |
PORT |
Standard 8080 |
CORS_ORIGINS |
Standard localhost/127.0.0.1 auf Port 5173 |
FORWARD_HEADERS_STRATEGY |
Standard native; Proxy-Header werden durch Tomcat ausgewertet |
SERVER_SERVLET_SESSION_COOKIE_SECURE |
Bei HTTPS in Produktion explizit true; ohne Vorgabe vom erkannten Request-Protokoll abhängig |
SESSION_TIMEOUT |
Ablauf bei Inaktivität, Standard 30m |
MAX_RESULTS |
Abfragegrenze, Standard 500, maximal 5000 |
Die Anwendung startet auch ohne erreichbare Datenbank. /api/health prüft die Anwendung; /api/system meldet die tatsächliche Datenbankerreichbarkeit. Eine fehlgeschlagene Datenabfrage oder Anmeldung bei fehlender DB-Verbindung liefert 503 und niemals erfundene oder leere Ersatzdaten. Die SQL-Kompatibilität muss zusätzlich am tatsächlichen Bestandsschema geprüft werden.
Anmeldung und Umfang
Die Anmeldung prüft ausschließlich vorhandene VoTiAn-Konten über user.usr_account, PASSWORD() und OLD_PASSWORD() und speichert die authentifizierte Identität in einer serverseitigen HTTP-Sitzung. Es erfolgt keine Passwortmigration und kein Update. Unterstützt sind vorhandene Zentralenbenutzer (usr_type=1) mit Mitarbeiter-, Niederlassungs- und Mandantenzuordnung. Keine Demo-Anmeldung und kein automatisch angelegtes Konto.
HTTP Basic ist im Backend deaktiviert. Ein vom Browser für den vorgeschalteten Nginx-Passwortschutz gesendeter Authorization-Header wird nicht zur VoTiAn-Anmeldung verwendet. VoTiAn erhält Benutzername und Passwort ausschließlich über POST /api/login; das Frontend speichert diese Werte nicht in sessionStorage oder localStorage.
Die Sitzung wird ausschließlich per Cookie VOTIANSESSION übertragen (HttpOnly, SameSite=Lax, Pfad /), nicht per URL. Ihre Kennung wechselt bei erfolgreicher Anmeldung. Nach standardmäßig 30 Minuten Inaktivität oder nach POST /api/logout ist die Sitzung ungültig. Sitzungen liegen im Backend-Arbeitsspeicher, benötigen keine Datenbanktabellen und gehen beim Neustart verloren. Mehrere Instanzen erfordern Sitzungsaffinität oder eine gemeinsame Sitzungsablage. Niederlassung und Rechte sind ein Snapshot bei Anmeldung; Änderungen im Bestand greifen für eine vorhandene Sitzung erst nach erneuter Anmeldung.
Ablauf für API-Clients
GET /api/csrfaufrufen und das dabei gesetzte Sitzungscookie behalten. Die JSON-Antwort enthälttoken,headerNameundparameterName.POST /api/loginmitContent-Type: application/x-www-form-urlencoded, den Formularfeldernusernameundpassword, dem Sitzungscookie und dem CSRF-Token im angegebenen Header senden. Erfolgreich:200mit denselben Benutzerinformationen wie/api/me; die Antwort erneuert die Sitzungskennung. Falsche Zugangsdaten liefern401, eine nicht verfügbare Datenbank503.- Für folgende API-Aufrufe das erneuerte Sitzungscookie verwenden.
GET /api/meprüft die bestehende Anmeldung; ohne gültige Anmeldung liefert es401und keine HTTP-Basic-Aufforderung. - Nach erfolgreicher Anmeldung und nach Abmeldung mit
GET /api/csrfeinen neuen Token beziehen, bevor erneut ein schreibender HTTP-Aufruf erfolgt. Die bisherigen Token werden dabei ungültig. Der Token gehört zur Sitzung und muss beiPOST,PUT,PATCHundDELETEim angegebenen Header mitgeschickt werden. - Zum Abmelden
POST /api/logoutmit gültigem CSRF-Token und Sitzungscookie senden. Die Antwort ist204; Sitzung und Cookie werden gelöscht.GET /api/logoutmeldet nicht ab.
Ein fehlender oder falscher CSRF-Token führt zu 403, auch bei Anmeldung und Abmeldung. Die React-Oberfläche übernimmt diesen Ablauf einschließlich erneuter Anmeldung bei abgelaufener Sitzung. Die CSRF-Token bleiben getrennt vom HttpOnly-Sitzungscookie. Hinter einem zusätzlichen Proxy-Passwortschutz bleiben dessen Zugangsdaten für alle Aufrufe notwendig; die VoTiAn-Abmeldung beendet den Proxy-Passwortschutz nicht. Siehe Spring Security: CSRF für JavaScript-Clients.
Betrieb hinter Nginx / Nginx Proxy Manager
Frontend und API unter derselben HTTPS-Origin bereitstellen und Cookies unverändert weiterleiten. Der Proxy muss X-Forwarded-Proto: https setzen und vom Client gelieferte Forwarded-Header ersetzen. Das Backend sollte nur vom vertrauenswürdigen Proxy erreichbar sein. FORWARD_HEADERS_STRATEGY=native verwendet Tomcats Proxy-Auswertung einschließlich seiner Vertrauensprüfung für interne Proxyadressen. Falls nötig, diese gezielt mit SERVER_TOMCAT_REMOTEIP_INTERNAL_PROXIES anpassen; keine leere Einstellung verwenden, die alle Proxies als vertrauenswürdig behandelt.
Im HTTPS-Produktionscontainer SERVER_SERVLET_SESSION_COOKIE_SECURE=true setzen. Dann funktionieren Sitzungsanmeldungen nur über HTTPS, auch wenn der interne Weg vom Proxy zum Backend HTTP verwendet. Für den lokalen HTTP-Start die Variable weglassen. Die Vite-Entwicklungsoberfläche verwendet ihren bestehenden /api-Proxy, sodass auch dort Oberfläche und API aus Browsersicht dieselbe Origin haben. Ein vorhandener Nginx-Basic-Passwortschutz kann aktiv bleiben; die Systemanmeldung verwendet dafür keinen Authorization-Header mehr.
Die ursprüngliche Niederlassung user.hq_id begrenzt alle Abfragen. Bei Aufträgen gilt die Erfassungsniederlassung job.hq_id; zusätzliche Dispositions-/Ausführungs-/Vertriebsniederlassungen werden nicht pauschal freigegeben. Kunden werden über customer.hq_id, Kuriere über courier.hq_id und user.hq_id beschränkt. Zusätzliche Rechteprüfungen verwenden die vorhandenen employee.emp_rights-Positionen: Kunden 0, Kuriere 1, Auftragslisten 4, Rechnung/CSV 5. Kunden- und Kurierlogins sowie Niederlassungswechsel sind noch nicht portiert.
API
| Route | Verhalten |
|---|---|
GET /api/health |
Öffentlich, Anwendungsbereitschaft |
GET /api/system |
Öffentlich, Verbindung, Funktionsumfang, Legacy-Link, Ergebnisgrenze |
GET /api/csrf |
Öffentlich, CSRF-Token und Header-/Parametername für die aktuelle Sitzung |
POST /api/login |
CSRF-geschützte Anmeldung mit Formularfeldern username und password; 200 mit Benutzerinformationen |
POST /api/logout |
CSRF-geschützte Abmeldung, Sitzung ungültig machen und Cookie löschen; 204 |
GET /api/me |
Bestehendes Konto, Niederlassung, verfügbare Lesebereiche |
GET /api/orders |
Aufträge mit Kundennamen; Filter q, status, customerId (effektiver Zahler), courierId, from, to |
GET /api/orders/{id} |
Auftrag derselben Erfassungsniederlassung mit Tourstationen |
GET /api/orders/{id}/history |
Vollständige Historie eines aktuellen Auftrags derselben Erfassungsniederlassung; Recht 4; aufsteigend nach Ereigniszeit und Log-ID |
GET /api/orders/export.csv |
Gefilterter UTF-8-Export, zusätzlich Recht 5; verweigert einen unvollständigen Export oberhalb der Ergebnisgrenze |
GET /api/customers, GET /api/couriers |
Stammdaten, optionaler Suchparameter q |
GET /api/invoices |
501, Bestands-Abrechnungsprozess noch nicht portiert |
Übrige Schreibmethoden unter /api/** |
501 nach erfolgreicher Anmeldung und gültiger CSRF-Prüfung; keine Datenänderung |
Listen liefern Arrays. Auftragslisten senden zusätzlich X-Total-Count und X-Result-Limit. from/to sind einschließlich, Format YYYY-MM-DD, bezogen auf jb_ordertime. Nicht belegte API-Felder bleiben null, insbesondere Stammdatenversionen, Rechnungs-ID, Lieferfrist, Packstückzahl und Distanz. Die UI soll diese als unbekannt anzeigen.
Fachliche Zuordnung und offene Arbeit
- Die Kundenanzeige (
customerId,customerName,costCenter), Namenssuche und der Kundenfilter folgenjob.csc_id_payer → costcenter → customer → company. Bei Barzahlung (jb_payment=2) mit positivercsc_id_payer_cashwird für diese Anzeige der tatsächliche Barzahler verwendet;payerCustomerIdbehält den Kunden auscsc_id_payer, also gegebenenfalls den technischen Systembarzahler. job.csc_idbezeichnet die erfassende Kundenkostenstelle und ist bei zentral erfassten Aufträgen regulär0. Dieser Bezug bleibt separat alsordererCustomerIderhalten. Er darf nicht als allgemeiner Kundenbezug verwendet werden. Quelle:jobs/job_edit.php:38–48,jobs/job_options.php:125–128,:1706–1711undadmin/jb_list.php:1022–1023.- Der Kundenname wird mit jedem sichtbaren Auftrag geliefert; die Anzeige ist damit unabhängig vom Ergebnislimit oder Leserecht der separaten Kundenstammliste. Die Auftragsabfrage bleibt über
job.hq_idund Recht 4 begrenzt. Nicht zuordenbare Kunden (beispielsweise Kostensplit ohne eindeutigen Zahler) bleibennullund werden in der Oberfläche ausdrücklich als nicht zugeordnet angezeigt. - Kundenpreis aus
jb_totalprice(Netto einschließlich Zuschlägen), Kurierpreis ausjb_cr_price; keine neu erfundene Tarifierung. - Status 0/1/2/8/9 bleiben zusätzlich als
legacyStatus/legacyStatusLabelerhalten. Die UI fasst 8/9 unter Disposition zusammen. jb_stornobleibt alslegacyStornoerhalten. 3/4 sind Gegenbuchungen und werden ausdrücklich als Storno-Gutschriften markiert (isCreditNote).- Tourrichtung 1=Abholung, 2=Zustellung, 3=beides (
BOTH), 0=positionsabhängige Legacy-Regel. - Leistungen aus der Bitmaske
jb_service, wobeimetatype.mt_sortdie Bitposition ist. Fahrzeugtypen werden übermetatypegelesen. - Historie wird beim Öffnen von „Verlauf“ separat über
/api/orders/{id}/historyausphoenix_log.loggeladen.logoperationliefert Vorgangsbezeichnungen,userden Bearbeiter. Die Oberfläche zeigt die neuesten Ereignisse zuerst und unterscheidet Laden, eine erfolgreich gelesene leere Historie und einen Zugriffsfehler mit Wiederholungsmöglichkeit. Das bisherigehistory-Feld in Auftragslisten/-details bleibt leer; maßgeblich ist der eigene Historien-Endpunkt. - Die Historienberechtigung folgt
job.hq_idund Recht 4; Ereignisse anderer Niederlassungen zum selben sichtbaren Auftrag bleiben erhalten. Fehlende/fremde Aufträge liefern 404, Datenbank-/Schemarechtefehler 503. Die Historie wird weder erfunden noch bei einem Fehler durch ein leeres Ergebnis ersetzt. Legacy-Details werden als Text mit bekannten deutschen Feldnamen angezeigt, unbekannte Angaben bleiben erhalten; gespeicherter HTML-Code wird nicht ausgeführt. Es erfolgen keine Schemaänderungen oder Datenbankschreibzugriffe. - Abrechnung im Bestand ist keine nachgewiesene einfache Invoice-CRUD-Tabelle. Belege, Gegenbuchungen, Zahlungsstatus, Tarifierung, Auftragsanlage/Änderung und Fahrerkommunikation bleiben bis zur vollständigen Portierung im vorhandenen PHP-System. Optional verlinkt die React-Oberfläche diese Module.
- Archivtabellen und Mehrniederlassungsrechte sind noch nicht eingebunden. Die bestehende Anwendung bleibt für diese Funktionen erforderlich.
SQL-/Mapping-Quellen im vorhandenen Code: include/mcglobal.inc.php, include/dbglobal.inc.php, include/caglobal.inc.php, admin/login.php, admin/menu.php, jobs/job_options.php, admin/jb_list.php und invoice/inv.inc.php.
Prüfung
./mvnw test führt Tests ohne Bestandszugriff aus: bestehende Anmeldung und Rechte, Sitzungs- und CSRF-Verhalten, HQ-Abgrenzung, SQL-Parameterbindung, Nichtverfügbarkeit, Verbot fachlicher Schreibpfade, serverseitige Vorbereitung der Lesetransaktion, Transaktions-/Fehlerverhalten, Schemadiagnose, Legacy-Status/Preis-/Tour-Mapping einschließlich JDBC-Boolean-Flags und CSV-Schutz. Normale Maven-Builds und Unit-Tests laden .env nicht. Ein Architekturtest prüft die kompilierten Java-Abhängigkeiten und Python-Launcher auf direkte Datenbankzugriffe außerhalb des Services. Nur dessen eigener Unit-Test darf JDBC mocken. Es wird keine Testdatenbank angelegt und keine Bestandsdatenbank verändert.
Mit VOTIAN_VERIFY_EXISTING_DB=true und den konfigurierten DB_URL, DB_USER, DB_PASSWORD wird zusätzlich LegacyDatabaseReadIntegrationTest ausgeführt. Er prüft die drei Listen-APIs mit echten SELECT-Abfragen über MockMvc, die numerische JDBC-Abbildung von TINYINT(1) und den Historien-Endpunkt gegen das konfigurierte Protokollschema. Für die Historie muss einer der neuesten 100 aktuellen Aufträge Protokolleinträge besitzen; geprüft werden Anzahl und Antwortformat ohne Ausgabe der Protokollinhalte. Die Authentifizierung wird nur in diesem Test simuliert; die echte Anmeldung wird damit nicht getestet. Die gewählte Niederlassung muss vorhandene Aufträge, Kunden und Kuriere enthalten. Ohne ausdrückliche Aktivierung bleibt dieser Test deaktiviert.
Für diesen bewusst aktivierten Integrationstest mit der lokalen .env im Verzeichnis backend/ ausführen:
VOTIAN_VERIFY_EXISTING_DB=true python3 ../scripts/env_config.py ./mvnw test
Der Integrationstest vergleicht außerdem die Kundenprojektion der neuesten 100 Aufträge mit unabhängigen Abfragen der Zahler-, Barzahler- und Erfassungskundenbezüge. Auch hierbei enthalten Testausgaben keine Kunden- oder Auftragsinhalte.