M0 Einrichtung
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.env.local
|
||||
.local/
|
||||
uploads/
|
||||
+8
-4
@@ -1,5 +1,9 @@
|
||||
<?php
|
||||
|
||||
if ((getenv('APP_DB_DRIVER') ?: '') === 'mysql' && !function_exists('sqlsrv_connect')) {
|
||||
require_once __DIR__ . '/lib/sqlsrv_mysql_compat.php';
|
||||
}
|
||||
|
||||
### ZUGANGSDATEN AD
|
||||
$aduser = '';
|
||||
$adpassword = '';
|
||||
@@ -23,11 +27,11 @@ $zugriffsalleAbwesenheitgroup = '';
|
||||
$zugriffsAdminsgroup = '';
|
||||
|
||||
// Verbindung zur Datenbank herstellen (ersetze die Platzhalter durch deine Daten)
|
||||
$serverName = "";
|
||||
$serverName = getenv('DB_HOST') ?: "";
|
||||
$connectionOptions = array(
|
||||
"Database" => "",
|
||||
"Uid" => "",
|
||||
"PWD" => "",
|
||||
"Database" => getenv('DB_NAME') ?: "",
|
||||
"Uid" => getenv('DB_USER') ?: "",
|
||||
"PWD" => getenv('DB_PASS') ?: "",
|
||||
"TrustServerCertificate"=>true
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
CREATE TABLE IF NOT EXISTS kl_Mitarbeiter (
|
||||
MitarbeiterID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
Name VARCHAR(255) NOT NULL,
|
||||
Email VARCHAR(255) NOT NULL,
|
||||
paypalname VARCHAR(255) NULL,
|
||||
aktiv TINYINT(1) NOT NULL DEFAULT 1,
|
||||
admin TINYINT(1) NOT NULL DEFAULT 0,
|
||||
UNIQUE KEY uq_kl_mitarbeiter_email (Email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kl_config (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
KostenproStrich DECIMAL(10,2) NOT NULL DEFAULT 0.20,
|
||||
paypaluse TINYINT(1) NOT NULL DEFAULT 0,
|
||||
paypallink VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
strichperweb TINYINT(1) NOT NULL DEFAULT 1
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kl_Einzahlungen (
|
||||
EinzahlungsID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
MitarbeiterID INT NOT NULL,
|
||||
Betrag DECIMAL(10,2) NOT NULL,
|
||||
Datum DATETIME NOT NULL,
|
||||
KEY idx_kl_einzahlungen_mitarbeiter_datum (MitarbeiterID, Datum),
|
||||
CONSTRAINT fk_kl_einzahlungen_mitarbeiter
|
||||
FOREIGN KEY (MitarbeiterID) REFERENCES kl_Mitarbeiter(MitarbeiterID)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kl_Kaffeeverbrauch (
|
||||
VerbrauchID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
MitarbeiterID INT NOT NULL,
|
||||
AnzahlStriche INT NOT NULL,
|
||||
Kosten DECIMAL(10,2) NOT NULL,
|
||||
KostenproStrich DECIMAL(10,2) NOT NULL,
|
||||
Datum DATETIME NOT NULL,
|
||||
Eintragsart INT NULL,
|
||||
KEY idx_kl_verbrauch_mitarbeiter_datum (MitarbeiterID, Datum),
|
||||
CONSTRAINT fk_kl_verbrauch_mitarbeiter
|
||||
FOREIGN KEY (MitarbeiterID) REFERENCES kl_Mitarbeiter(MitarbeiterID)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kl_hinweise (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nachricht TEXT NOT NULL,
|
||||
gueltig_bis DATETIME NOT NULL,
|
||||
KEY idx_kl_hinweise_gueltig_bis (gueltig_bis)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS CoffeeSurveyVotedEmails (
|
||||
EmailNorm VARCHAR(255) PRIMARY KEY,
|
||||
CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS CoffeeSurveyResponses (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
Q1_ListEase INT NOT NULL,
|
||||
Q2_ListSatisfaction INT NOT NULL,
|
||||
Q3_CoffeeQuality INT NOT NULL,
|
||||
Q4_WebsiteSatisfaction INT NOT NULL,
|
||||
Q5_Drinks TEXT NOT NULL,
|
||||
Q5_DrinksOther TEXT NULL,
|
||||
Q6_NewVarieties TEXT NULL,
|
||||
Q7_ListProblem VARCHAR(100) NOT NULL,
|
||||
Q7_ListProblemOther TEXT NULL,
|
||||
Q8_Improvements TEXT NOT NULL,
|
||||
Q8_ImprovementsOther TEXT NULL,
|
||||
Q9_BetterIdeas TEXT NULL,
|
||||
CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO kl_config (id, KostenproStrich, paypaluse, paypallink, strichperweb)
|
||||
VALUES (1, 0.20, 0, '', 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
KostenproStrich = VALUES(KostenproStrich),
|
||||
paypaluse = VALUES(paypaluse),
|
||||
strichperweb = VALUES(strichperweb);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Lokaler MySQL-Dev-Start
|
||||
|
||||
Die Legacy-App kann fuer Entwicklung gegen MySQL laufen. Dafuer gibt es eine
|
||||
Kompatibilitaetsschicht, die die verwendeten `sqlsrv_*`-Funktionen im Dev-Modus
|
||||
auf PDO/MySQL abbildet.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- PHP CLI mit `pdo_mysql`.
|
||||
- MySQL-Testdatenbank.
|
||||
- Keine Produktivdaten und keine Produktivpasswoerter im Repository.
|
||||
|
||||
In dieser VS-Code-Server-Umgebung wurde PHP lokal unter `.local/` entpackt,
|
||||
weil `sudo` nicht passwordless verfuegbar war. `.local/` wird von Git ignoriert.
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
```bash
|
||||
export APP_DB_DRIVER=mysql
|
||||
export DB_HOST="example.mysql.host"
|
||||
export DB_PORT="3306"
|
||||
export DB_NAME="example_database"
|
||||
export DB_USER="example_user"
|
||||
export DB_PASS="example_password"
|
||||
export DEV_AUTH_EMAIL="admin@test.local"
|
||||
export DEV_AUTH_NAME="Test Admin"
|
||||
```
|
||||
|
||||
## Testdatenbank initialisieren
|
||||
|
||||
```bash
|
||||
php scripts/init-mysql-dev.php
|
||||
```
|
||||
|
||||
In der aktuellen lokalen Umgebung:
|
||||
|
||||
```bash
|
||||
LD_LIBRARY_PATH="$PWD/.local/php/usr/lib/x86_64-linux-gnu" \
|
||||
"$PWD/.local/php/usr/bin/php8.3" \
|
||||
-c "$PWD/.local/php-dev.ini" \
|
||||
scripts/init-mysql-dev.php
|
||||
```
|
||||
|
||||
## Server starten
|
||||
|
||||
```bash
|
||||
HOST=127.0.0.1 PORT=8080 scripts/run-dev-server.sh
|
||||
```
|
||||
|
||||
Danach ist die App unter `http://127.0.0.1:8080/` erreichbar. In VS Code kann
|
||||
der Port ueber die Ports-Ansicht weitergeleitet werden.
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Der Dev-Login umgeht AD/LDAP und nutzt `DEV_AUTH_EMAIL`.
|
||||
- Die Tabellen werden aus `database/mysql-dev-schema.sql` angelegt.
|
||||
- Der Dev-Modus ist nur fuer lokale Tests gedacht, nicht fuer Produktion.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# M0 Baseline
|
||||
|
||||
Stand: 2026-07-11
|
||||
|
||||
M0 dokumentiert den aktuellen Legacy-Bestand, bevor die SaaS-Umstrukturierung
|
||||
beginnt. Ziel ist eine belastbare Ausgangsbasis fuer Migration, Sicherheit,
|
||||
Design-Erhalt und fachliche Vergleichstests.
|
||||
|
||||
## Status
|
||||
|
||||
| Bereich | Status | Ergebnis |
|
||||
| --- | --- | --- |
|
||||
| Code-Inventar | begonnen | Siehe `code-inventory.md` |
|
||||
| Prozesslandkarte | begonnen | In `code-inventory.md` enthalten |
|
||||
| Sicherheitsbaseline | begonnen | Siehe `security-baseline.md` |
|
||||
| DB-Schema-Export | vorbereitet | Siehe `schema-export.sql` |
|
||||
| Golden-Master-Auswertungen | vorbereitet | Siehe `golden-master-queries.sql` |
|
||||
| Design-/Screenshot-Referenz | vorbereitet | Siehe `screenshot-checklist.md` |
|
||||
|
||||
## Lokal abgeschlossen
|
||||
|
||||
- Legacy-Einstiegspunkte und Kernseiten identifiziert.
|
||||
- Fachliche Tabellen aus SQL-Strings abgeleitet.
|
||||
- Schreibende Aktionen und kritische Datenfluesse markiert.
|
||||
- Sicherheitsrisiken fuer M0 inventarisiert.
|
||||
- SQL-Vorlagen fuer Schema- und Golden-Master-Export angelegt.
|
||||
- Screenshot-Checkliste fuer Design-Erhalt angelegt.
|
||||
|
||||
## Lokale Einschraenkungen
|
||||
|
||||
- `php` ist in dieser Umgebung nicht im PATH, daher kein lokaler Syntaxcheck und
|
||||
kein lokaler App-Start.
|
||||
- `sqlcmd` ist in dieser Umgebung nicht im PATH, daher kein direkter
|
||||
SQL-Server-Export aus der Shell.
|
||||
- Die aktuelle M0-Auswertung ist deshalb statisch aus dem Code abgeleitet.
|
||||
|
||||
## Blockiert bis Input vorliegt
|
||||
|
||||
Diese Punkte kann ich ohne externe Informationen nicht abschliessen:
|
||||
|
||||
1. Live- oder Staging-DB-Schema exportieren.
|
||||
2. Tabellen, Spalten, Indizes, Constraints und Rowcounts verifizieren.
|
||||
3. Golden-Master-Ergebnisse gegen echte Daten berechnen.
|
||||
4. Screenshots der echten App-Zustaende aufnehmen, falls die App lokal nicht
|
||||
lauffaehig ist.
|
||||
5. Produktivumgebung dokumentieren: PHP-Version, Webserver, SQL-Server-Version,
|
||||
Auth-Setup, Cron/Job-Ausfuehrung.
|
||||
|
||||
## Was ich von dir brauche
|
||||
|
||||
Bitte stelle eines der folgenden Pakete bereit:
|
||||
|
||||
- Idealerweise: einen anonymisierten SQL-Server-Schemaexport plus Rowcounts.
|
||||
- Alternativ: Zugriffsdaten zu einer Test-/Staging-DB, nicht zu Produktion.
|
||||
- Alternativ: die Ergebnisse aus `schema-export.sql` als CSV/Excel/Markdown.
|
||||
|
||||
Zusaetzlich hilfreich:
|
||||
|
||||
- Ein erreichbarer Test-Link zur bestehenden App oder Screenshots der Seiten aus
|
||||
`screenshot-checklist.md`.
|
||||
- Beispiel-Accounts oder Rollen fuer Admin und normales Mitglied, nur fuer eine
|
||||
Testumgebung.
|
||||
- Info, ob hart codierte Zugangsdaten aus Legacy-Skripten bereits rotiert wurden.
|
||||
- Entscheidung, ob M1/M2 nativ in PHP oder mit Framework vorbereitet werden
|
||||
sollen.
|
||||
|
||||
Bitte keine Produktivpasswoerter oder echten personenbezogenen Exportdaten in
|
||||
das Repository legen.
|
||||
@@ -0,0 +1,192 @@
|
||||
# M0 Code- und Prozessinventar
|
||||
|
||||
Stand: 2026-07-11
|
||||
|
||||
Dieses Inventar basiert auf statischer Sichtung des Legacy-Codes im Repository.
|
||||
Das echte Datenbankschema muss noch mit `schema-export.sql` verifiziert werden.
|
||||
|
||||
## App-Struktur
|
||||
|
||||
| Datei | Rolle | Wichtige Beobachtung |
|
||||
| --- | --- | --- |
|
||||
| `config.php` | AD-/DB-Konfiguration | Erstellt globale SQL-Server-Verbindung `$conn` |
|
||||
| `functions.php` | Auth-/User-Helper | Liest `$_SERVER['AUTH_USER']`, inkludiert LDAP, prueft `kl_Mitarbeiter` |
|
||||
| `functionsLDAP.php` | LDAP-Mailauflösung | Ermittelt `$mailadress` aus AD |
|
||||
| `header.php` | HTML-Head und Hinweisbanner | Laedt `assets/css/main.css`, liest `kl_hinweise` |
|
||||
| `footer.php` | App-Sidebar und JS | Enthaelt die sichtbare Navigation und Admin-Menues |
|
||||
| `nav.php` | Navigation | Aktuell praktisch leer |
|
||||
| `headerline.php` | Headerfragment | Enthaelt NUL-Zeichen, fachlich unklar |
|
||||
| `assets/css/main.css` | Hauptdesign | HTML5-UP Editorial, gruener Akzent, Tabellen, Buttons, Sidebar |
|
||||
| `DataTables/` | Tabellen-Assets | Wird teilweise eingebunden, aber nicht konsequent initialisiert |
|
||||
| `PHPMailer/` | Mailversand | Fuer Rundmails/Jahresauswertung genutzt |
|
||||
| `TCPDF/` | PDF-Export | Fuer Kaffeelisten-Export genutzt |
|
||||
|
||||
## Fachliche Kernseiten
|
||||
|
||||
| Datei | Prozess | Tabellen |
|
||||
| --- | --- | --- |
|
||||
| `index.php` | Persoenliches Dashboard, Saldo, Jahreswerte, eigene Striche, PayPal-Links | `kl_Mitarbeiter`, `kl_Einzahlungen`, `kl_Kaffeeverbrauch`, `kl_config` |
|
||||
| `kaffeeliste.php` | Admin-Gesamtuebersicht mit Salden | `kl_Mitarbeiter`, `kl_Einzahlungen`, `kl_Kaffeeverbrauch` |
|
||||
| `stricheintragen.php` | Sammelerfassung von Strichen | `kl_Mitarbeiter`, `kl_Kaffeeverbrauch`, `kl_config` |
|
||||
| `einzahlung.php` | Sammelerfassung von Einzahlungen | `kl_Mitarbeiter`, `kl_Einzahlungen`, `kl_Kaffeeverbrauch` |
|
||||
| `mitarbeiterverwalten.php` | Mitglieder anlegen, bearbeiten, aktivieren, deaktivieren | `kl_Mitarbeiter` |
|
||||
| `namenanpassen.php` | Anzeigenamen aendern | `kl_Mitarbeiter` |
|
||||
| `letzteneintraege.php` | Letzte Buchungen anzeigen und loeschen | `kl_Einzahlungen`, `kl_Kaffeeverbrauch`, `kl_Mitarbeiter` |
|
||||
| `csvupload.php` | Zahlungs-CSV importieren | `kl_Mitarbeiter`, `kl_Einzahlungen` |
|
||||
| `exportKaffeeliste.php` | PDF-/Listenexport | `kl_Mitarbeiter`, `kl_Kaffeeverbrauch`, `kl_Einzahlungen`, `kl_config` |
|
||||
| `mailausgebe.php` | Mailausgabe/Adressliste | `kl_Mitarbeiter` |
|
||||
| `mailversenden.php` | Massenmail mit aktuellem Stand | `kl_Mitarbeiter`, `kl_Einzahlungen`, `kl_Kaffeeverbrauch` |
|
||||
| `hinweise.php` | Hinweise pflegen | `kl_hinweise` |
|
||||
| `faq.php` | FAQ | Keine fachlichen Schreibzugriffe |
|
||||
| `jahresauswertung.php` | Jahresgutschriften und Mailversand | `kl_Mitarbeiter`, `kl_Kaffeeverbrauch`, `kl_Einzahlungen` |
|
||||
| `umfrage.php` | Umfrage | `CoffeeSurveyVotedEmails`, `CoffeeSurveyResponses` |
|
||||
| `umfrageergebnisse.php` | Umfrageauswertung | `CoffeeSurveyVotedEmails`, `CoffeeSurveyResponses` |
|
||||
|
||||
## Abgeleitete Tabellen und Spalten
|
||||
|
||||
Diese Spalten sind aus dem Code abgeleitet und muessen mit dem echten DB-Schema
|
||||
abgeglichen werden.
|
||||
|
||||
| Tabelle | Abgeleitete Spalten |
|
||||
| --- | --- |
|
||||
| `kl_Mitarbeiter` | `MitarbeiterID`, `Name`, `Email`, `aktiv`, `admin`, `paypalname` |
|
||||
| `kl_Einzahlungen` | `EinzahlungsID`, `MitarbeiterID`, `Betrag`, `Datum` |
|
||||
| `kl_Kaffeeverbrauch` | `VerbrauchID`, `MitarbeiterID`, `AnzahlStriche`, `Kosten`, `KostenproStrich`, `Datum`, `Eintragsart` |
|
||||
| `kl_config` | `KostenproStrich`, `paypaluse`, `paypallink`, `strichperweb` |
|
||||
| `kl_hinweise` | `id`, `nachricht`, `gueltig_bis` |
|
||||
| `CoffeeSurveyVotedEmails` | `EmailNorm` |
|
||||
| `CoffeeSurveyResponses` | diverse `Q*_`-Antwortspalten, siehe Umfragecode |
|
||||
|
||||
## Prozesslandkarte
|
||||
|
||||
### Authentifizierung
|
||||
|
||||
1. Seite inkludiert `functions.php`.
|
||||
2. `functions.php` liest `$_SERVER['AUTH_USER']`.
|
||||
3. `functionsLDAP.php` ermittelt Mailadresse aus LDAP.
|
||||
4. Zugriff wird ueber `kl_Mitarbeiter.Email`, `aktiv` und `admin` geprueft.
|
||||
|
||||
M0-Feststellung:
|
||||
Login-Konto, Kaffee-Teilnehmer und Rolle sind im Legacy-Modell gekoppelt. Fuer
|
||||
SaaS muessen `users`, `participants` und `tenant_memberships` getrennt werden.
|
||||
|
||||
### Persoenliches Dashboard
|
||||
|
||||
Quelle: `index.php`.
|
||||
|
||||
Berechnungen:
|
||||
|
||||
- Mitarbeiter per E-Mail suchen.
|
||||
- Gesamteinzahlung: Summe `kl_Einzahlungen.Betrag`.
|
||||
- Gesamtausgabe: Summe `kl_Kaffeeverbrauch.Kosten`.
|
||||
- Gesamtstriche: Summe `kl_Kaffeeverbrauch.AnzahlStriche`.
|
||||
- Aktueller Stand: Einzahlungen minus Ausgaben.
|
||||
- Jahreswerte: dieselben Summen fuer aktuelles Jahr.
|
||||
- Bei aktivem `strichperweb`: eigener Strich-Eintrag.
|
||||
- Bei aktivem `paypaluse`: PayPal-Links anzeigen.
|
||||
|
||||
### Sammelerfassung Striche
|
||||
|
||||
Quelle: `stricheintragen.php`.
|
||||
|
||||
Berechnungen:
|
||||
|
||||
- Preis pro Strich aus `kl_config.KostenproStrich`.
|
||||
- Pro Mitarbeiter eine Anzahl erfassen.
|
||||
- Kosten = Anzahl Striche mal Kosten pro Strich.
|
||||
- Neue Zeile in `kl_Kaffeeverbrauch`.
|
||||
- Filter `vorderseite`/`rueckseite` anhand der letzten 100 Tage und mindestens
|
||||
10 Strichen.
|
||||
|
||||
### Sammelerfassung Einzahlungen
|
||||
|
||||
Quelle: `einzahlung.php`.
|
||||
|
||||
Berechnungen:
|
||||
|
||||
- Pro Mitarbeiter Betrag erfassen.
|
||||
- Neue Zeile in `kl_Einzahlungen`.
|
||||
- Filter `vorderseite`/`rueckseite` anhand der letzten 100 Tage und mindestens
|
||||
10 Strichen.
|
||||
|
||||
### Mitgliederverwaltung
|
||||
|
||||
Quelle: `mitarbeiterverwalten.php`.
|
||||
|
||||
Aktionen:
|
||||
|
||||
- Mitglied anlegen.
|
||||
- Name/E-Mail/aktiv/admin bearbeiten.
|
||||
- Mitglied aktivieren/deaktivieren.
|
||||
|
||||
M0-Feststellung:
|
||||
Das `admin`-Flag ist fachlich eine Rolleninformation und sollte in der SaaS-App
|
||||
in `tenant_memberships.role` wandern.
|
||||
|
||||
### Korrektur letzter Eintraege
|
||||
|
||||
Quelle: `letzteneintraege.php`.
|
||||
|
||||
Aktionen:
|
||||
|
||||
- Letzte 100 Einzahlungen anzeigen.
|
||||
- Einzahlung hart loeschen.
|
||||
- Letzte 100 Strich-Eintraege anzeigen.
|
||||
- Strich-Eintrag hart loeschen.
|
||||
|
||||
M0-Feststellung:
|
||||
Finanznahe Korrekturen sollten im Zielmodell nicht hart loeschen, sondern als
|
||||
Storno/Reversal mit Audit-Trail gespeichert werden.
|
||||
|
||||
### CSV-Import
|
||||
|
||||
Quelle: `csvupload.php`.
|
||||
|
||||
Ablauf:
|
||||
|
||||
- Datei hochladen.
|
||||
- CSV mit Komma-Trennung lesen.
|
||||
- Name aus Spalte 3, Betrag aus Spalte 7, Datum aus Spalte 0.
|
||||
- Mitarbeiter per Name oder `paypalname` finden.
|
||||
- Dublette anhand Mitarbeiter, Betrag und Datumstag pruefen.
|
||||
- Einzahlung einfuegen.
|
||||
- Ergebnis in Erfolg/Fehler klassifizieren.
|
||||
|
||||
### Jahresauswertung
|
||||
|
||||
Quelle: `jahresauswertung.php`.
|
||||
|
||||
Ablauf:
|
||||
|
||||
- Gesamtstriche des aktuellen Jahres je aktivem Mitarbeiter berechnen.
|
||||
- Konstante Anzahl neuer Striche/Gutschrift proportional verteilen.
|
||||
- Einzahlung je Mitarbeiter einfuegen.
|
||||
- Mail an Mitarbeiter senden.
|
||||
|
||||
M0-Feststellung:
|
||||
Das ist ein Job/Skript mit direktem Schreibzugriff und Mailversand. Fuer SaaS
|
||||
braucht es Dry-Run, Audit, Tenant-Scope und expliziten Operator.
|
||||
|
||||
## Offene Schemafragen
|
||||
|
||||
- Gibt es Primary Keys, Foreign Keys und Indizes fuer die `kl_`-Tabellen?
|
||||
- Sind Datentypen fuer Geldwerte `decimal`, `money`, `float` oder etwas anderes?
|
||||
- Gibt es historische Spalten, die im Code nicht sichtbar sind?
|
||||
- Gibt es Trigger oder Stored Procedures?
|
||||
- Gibt es weitere Jobs ausser `jahresauswertung.php`?
|
||||
- Sind Umfragetabellen produktiv relevant oder Archiv/Randmodul?
|
||||
|
||||
## Technische Legacy-Inkonsistenzen
|
||||
|
||||
- `csvupload.php` sucht in `getMitarbeiterID` nach `Name = ? or paypalname = ?`,
|
||||
uebergibt aber nur einen Parameter. Das sollte vor der Uebernahme des
|
||||
CSV-Flows verifiziert werden.
|
||||
- `einzahlung.php` prueft auf `$_GET["action"]`, die Buttons erzeugen aber
|
||||
URLs mit `aktion=...`. Der Vorderseite-/Rueckseite-Filter kann dadurch
|
||||
abweichend vom erwarteten Verhalten laufen.
|
||||
- `exportKaffeeliste.php` referenziert `tcpdf/tcpdf.php`, waehrend der Ordner im
|
||||
Repository `TCPDF/` heisst. Auf case-sensitiven Dateisystemen kann das
|
||||
brechen.
|
||||
- `headerline.php` ist fachlich leer beziehungsweise mit NUL-Zeichen gefuellt.
|
||||
Vor der neuen App-Shell sollte geklaert werden, ob die Datei entfernt werden
|
||||
kann.
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
M0 Golden-Master Queries
|
||||
|
||||
Ziel:
|
||||
- Fachliche Legacy-Ergebnisse exportieren, damit Migration und neue SaaS-App
|
||||
gegen denselben Stand verglichen werden koennen.
|
||||
|
||||
Ausfuehrung:
|
||||
- In der Legacy-Datenbank ausfuehren.
|
||||
- Ergebnisse je Query als CSV speichern.
|
||||
- Keine personenbezogenen Exporte ins Repo committen.
|
||||
*/
|
||||
|
||||
DECLARE @CurrentYear int = YEAR(GETDATE());
|
||||
|
||||
-- 1. Teilnehmer-Basisdaten
|
||||
SELECT
|
||||
m.MitarbeiterID,
|
||||
m.Name,
|
||||
m.Email,
|
||||
m.aktiv,
|
||||
m.admin
|
||||
FROM kl_Mitarbeiter m
|
||||
ORDER BY m.Name;
|
||||
|
||||
-- 2. Saldo je Teilnehmer
|
||||
WITH payments AS (
|
||||
SELECT
|
||||
MitarbeiterID,
|
||||
SUM(Betrag) AS total_payments
|
||||
FROM kl_Einzahlungen
|
||||
GROUP BY MitarbeiterID
|
||||
),
|
||||
consumption AS (
|
||||
SELECT
|
||||
MitarbeiterID,
|
||||
SUM(Kosten) AS total_costs,
|
||||
SUM(AnzahlStriche) AS total_marks
|
||||
FROM kl_Kaffeeverbrauch
|
||||
GROUP BY MitarbeiterID
|
||||
)
|
||||
SELECT
|
||||
m.MitarbeiterID,
|
||||
m.Name,
|
||||
m.Email,
|
||||
COALESCE(p.total_payments, 0) AS total_payments,
|
||||
COALESCE(c.total_costs, 0) AS total_costs,
|
||||
COALESCE(c.total_marks, 0) AS total_marks,
|
||||
COALESCE(p.total_payments, 0) - COALESCE(c.total_costs, 0) AS balance
|
||||
FROM kl_Mitarbeiter m
|
||||
LEFT JOIN payments p ON p.MitarbeiterID = m.MitarbeiterID
|
||||
LEFT JOIN consumption c ON c.MitarbeiterID = m.MitarbeiterID
|
||||
ORDER BY m.Name;
|
||||
|
||||
-- 3. Jahreswerte je Teilnehmer
|
||||
WITH year_payments AS (
|
||||
SELECT
|
||||
MitarbeiterID,
|
||||
SUM(Betrag) AS year_payments
|
||||
FROM kl_Einzahlungen
|
||||
WHERE YEAR(Datum) = @CurrentYear
|
||||
GROUP BY MitarbeiterID
|
||||
),
|
||||
year_consumption AS (
|
||||
SELECT
|
||||
MitarbeiterID,
|
||||
SUM(Kosten) AS year_costs,
|
||||
SUM(AnzahlStriche) AS year_marks
|
||||
FROM kl_Kaffeeverbrauch
|
||||
WHERE YEAR(Datum) = @CurrentYear
|
||||
GROUP BY MitarbeiterID
|
||||
)
|
||||
SELECT
|
||||
m.MitarbeiterID,
|
||||
m.Name,
|
||||
m.Email,
|
||||
COALESCE(yp.year_payments, 0) AS year_payments,
|
||||
COALESCE(yc.year_costs, 0) AS year_costs,
|
||||
COALESCE(yc.year_marks, 0) AS year_marks
|
||||
FROM kl_Mitarbeiter m
|
||||
LEFT JOIN year_payments yp ON yp.MitarbeiterID = m.MitarbeiterID
|
||||
LEFT JOIN year_consumption yc ON yc.MitarbeiterID = m.MitarbeiterID
|
||||
ORDER BY m.Name;
|
||||
|
||||
-- 4. Letzte Einzahlungen je Teilnehmer
|
||||
SELECT
|
||||
e.EinzahlungsID,
|
||||
e.MitarbeiterID,
|
||||
m.Name,
|
||||
m.Email,
|
||||
e.Betrag,
|
||||
e.Datum
|
||||
FROM kl_Einzahlungen e
|
||||
JOIN kl_Mitarbeiter m ON m.MitarbeiterID = e.MitarbeiterID
|
||||
ORDER BY e.Datum DESC, e.EinzahlungsID DESC;
|
||||
|
||||
-- 5. Letzte Strich-/Verbrauchseintraege je Teilnehmer
|
||||
SELECT
|
||||
v.VerbrauchID,
|
||||
v.MitarbeiterID,
|
||||
m.Name,
|
||||
m.Email,
|
||||
v.AnzahlStriche,
|
||||
v.Kosten,
|
||||
v.KostenproStrich,
|
||||
v.Datum,
|
||||
v.Eintragsart
|
||||
FROM kl_Kaffeeverbrauch v
|
||||
JOIN kl_Mitarbeiter m ON m.MitarbeiterID = v.MitarbeiterID
|
||||
ORDER BY v.Datum DESC, v.VerbrauchID DESC;
|
||||
|
||||
-- 6. Aktuelle Konfiguration
|
||||
SELECT
|
||||
*
|
||||
FROM kl_config;
|
||||
|
||||
-- 7. Aktuelle und historische Hinweise
|
||||
SELECT
|
||||
id,
|
||||
nachricht,
|
||||
gueltig_bis
|
||||
FROM kl_hinweise
|
||||
ORDER BY gueltig_bis DESC;
|
||||
|
||||
-- 8. 100-Tage-Vorderseite analog Legacy-Stricherfassung
|
||||
DECLARE @ReferenceDate date = (
|
||||
SELECT MAX(CAST(Datum AS date))
|
||||
FROM kl_Kaffeeverbrauch
|
||||
WHERE Datum < CAST(GETDATE() AS date)
|
||||
);
|
||||
|
||||
SELECT
|
||||
m.MitarbeiterID,
|
||||
m.Name,
|
||||
m.Email,
|
||||
SUM(v.AnzahlStriche) AS marks_in_window
|
||||
FROM kl_Mitarbeiter m
|
||||
JOIN kl_Kaffeeverbrauch v ON m.MitarbeiterID = v.MitarbeiterID
|
||||
WHERE v.Datum >= DATEADD(DAY, -100, @ReferenceDate)
|
||||
AND m.aktiv = 1
|
||||
GROUP BY m.MitarbeiterID, m.Name, m.Email
|
||||
HAVING SUM(v.AnzahlStriche) >= 10
|
||||
ORDER BY m.Name;
|
||||
|
||||
-- 9. 100-Tage-Rueckseite analog Legacy-Stricherfassung
|
||||
SELECT
|
||||
m.MitarbeiterID,
|
||||
m.Name,
|
||||
m.Email,
|
||||
COALESCE(SUM(v.AnzahlStriche), 0) AS marks_in_window
|
||||
FROM kl_Mitarbeiter m
|
||||
LEFT JOIN kl_Kaffeeverbrauch v
|
||||
ON m.MitarbeiterID = v.MitarbeiterID
|
||||
AND v.Datum >= DATEADD(DAY, -100, @ReferenceDate)
|
||||
WHERE m.aktiv = 1
|
||||
GROUP BY m.MitarbeiterID, m.Name, m.Email
|
||||
HAVING COALESCE(SUM(v.AnzahlStriche), 0) < 10
|
||||
ORDER BY m.Name;
|
||||
|
||||
-- 10. CSV-Import-Dublettenbasis
|
||||
SELECT
|
||||
e.MitarbeiterID,
|
||||
m.Name,
|
||||
e.Betrag,
|
||||
CONVERT(varchar, e.Datum, 23) AS booking_day,
|
||||
COUNT(*) AS duplicates_per_day
|
||||
FROM kl_Einzahlungen e
|
||||
JOIN kl_Mitarbeiter m ON m.MitarbeiterID = e.MitarbeiterID
|
||||
GROUP BY e.MitarbeiterID, m.Name, e.Betrag, CONVERT(varchar, e.Datum, 23)
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY booking_day DESC, m.Name;
|
||||
|
||||
-- 11. Jahresauswertung: Gesamtstriche aktuelles Jahr
|
||||
SELECT
|
||||
m.MitarbeiterID,
|
||||
m.Name,
|
||||
m.Email,
|
||||
SUM(v.AnzahlStriche) AS GesamtStriche
|
||||
FROM kl_Kaffeeverbrauch v
|
||||
JOIN kl_Mitarbeiter m ON v.MitarbeiterID = m.MitarbeiterID
|
||||
WHERE YEAR(v.Datum) = @CurrentYear
|
||||
AND m.aktiv = 1
|
||||
GROUP BY m.MitarbeiterID, m.Name, m.Email
|
||||
ORDER BY GesamtStriche DESC, m.Name;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
M0 SQL Server Schema Export
|
||||
|
||||
Ziel:
|
||||
- Tabellen, Spalten, Defaults, Primary Keys, Foreign Keys, Indizes und Rowcounts
|
||||
aus der Legacy-Datenbank exportieren.
|
||||
|
||||
Hinweise:
|
||||
- In der Ziel-Datenbank ausfuehren.
|
||||
- Ergebnisse als CSV/Excel speichern und nicht mit Produktiv-Secrets ins Repo
|
||||
legen.
|
||||
- Falls nur Kaffeelisten-Tabellen exportiert werden sollen, den WHERE-Block bei
|
||||
Bedarf auf kl_% und CoffeeSurvey% begrenzen.
|
||||
*/
|
||||
|
||||
-- 1. Tabellen und Spalten
|
||||
SELECT
|
||||
s.name AS schema_name,
|
||||
t.name AS table_name,
|
||||
c.column_id,
|
||||
c.name AS column_name,
|
||||
ty.name AS data_type,
|
||||
c.max_length,
|
||||
c.precision,
|
||||
c.scale,
|
||||
c.is_nullable,
|
||||
c.is_identity,
|
||||
dc.definition AS default_definition,
|
||||
ep.value AS column_description
|
||||
FROM sys.tables t
|
||||
JOIN sys.schemas s ON s.schema_id = t.schema_id
|
||||
JOIN sys.columns c ON c.object_id = t.object_id
|
||||
JOIN sys.types ty ON ty.user_type_id = c.user_type_id
|
||||
LEFT JOIN sys.default_constraints dc
|
||||
ON dc.parent_object_id = t.object_id
|
||||
AND dc.parent_column_id = c.column_id
|
||||
LEFT JOIN sys.extended_properties ep
|
||||
ON ep.major_id = t.object_id
|
||||
AND ep.minor_id = c.column_id
|
||||
AND ep.name = 'MS_Description'
|
||||
WHERE t.is_ms_shipped = 0
|
||||
ORDER BY s.name, t.name, c.column_id;
|
||||
|
||||
-- 2. Primary Keys und Unique Constraints
|
||||
SELECT
|
||||
s.name AS schema_name,
|
||||
t.name AS table_name,
|
||||
kc.name AS constraint_name,
|
||||
kc.type_desc,
|
||||
ic.key_ordinal,
|
||||
c.name AS column_name
|
||||
FROM sys.key_constraints kc
|
||||
JOIN sys.tables t ON t.object_id = kc.parent_object_id
|
||||
JOIN sys.schemas s ON s.schema_id = t.schema_id
|
||||
JOIN sys.index_columns ic
|
||||
ON ic.object_id = kc.parent_object_id
|
||||
AND ic.index_id = kc.unique_index_id
|
||||
JOIN sys.columns c
|
||||
ON c.object_id = ic.object_id
|
||||
AND c.column_id = ic.column_id
|
||||
ORDER BY s.name, t.name, kc.name, ic.key_ordinal;
|
||||
|
||||
-- 3. Foreign Keys
|
||||
SELECT
|
||||
sch_parent.name AS parent_schema,
|
||||
parent_t.name AS parent_table,
|
||||
parent_c.name AS parent_column,
|
||||
fk.name AS foreign_key_name,
|
||||
sch_ref.name AS referenced_schema,
|
||||
ref_t.name AS referenced_table,
|
||||
ref_c.name AS referenced_column,
|
||||
fk.delete_referential_action_desc,
|
||||
fk.update_referential_action_desc
|
||||
FROM sys.foreign_keys fk
|
||||
JOIN sys.foreign_key_columns fkc ON fkc.constraint_object_id = fk.object_id
|
||||
JOIN sys.tables parent_t ON parent_t.object_id = fkc.parent_object_id
|
||||
JOIN sys.schemas sch_parent ON sch_parent.schema_id = parent_t.schema_id
|
||||
JOIN sys.columns parent_c
|
||||
ON parent_c.object_id = fkc.parent_object_id
|
||||
AND parent_c.column_id = fkc.parent_column_id
|
||||
JOIN sys.tables ref_t ON ref_t.object_id = fkc.referenced_object_id
|
||||
JOIN sys.schemas sch_ref ON sch_ref.schema_id = ref_t.schema_id
|
||||
JOIN sys.columns ref_c
|
||||
ON ref_c.object_id = fkc.referenced_object_id
|
||||
AND ref_c.column_id = fkc.referenced_column_id
|
||||
ORDER BY parent_schema, parent_table, foreign_key_name;
|
||||
|
||||
-- 4. Indizes
|
||||
SELECT
|
||||
s.name AS schema_name,
|
||||
t.name AS table_name,
|
||||
i.name AS index_name,
|
||||
i.type_desc,
|
||||
i.is_unique,
|
||||
i.is_primary_key,
|
||||
i.is_unique_constraint,
|
||||
ic.key_ordinal,
|
||||
ic.is_included_column,
|
||||
c.name AS column_name
|
||||
FROM sys.indexes i
|
||||
JOIN sys.tables t ON t.object_id = i.object_id
|
||||
JOIN sys.schemas s ON s.schema_id = t.schema_id
|
||||
JOIN sys.index_columns ic
|
||||
ON ic.object_id = i.object_id
|
||||
AND ic.index_id = i.index_id
|
||||
JOIN sys.columns c
|
||||
ON c.object_id = ic.object_id
|
||||
AND c.column_id = ic.column_id
|
||||
WHERE t.is_ms_shipped = 0
|
||||
AND i.name IS NOT NULL
|
||||
ORDER BY s.name, t.name, i.name, ic.key_ordinal, ic.index_column_id;
|
||||
|
||||
-- 5. Rowcounts
|
||||
SELECT
|
||||
s.name AS schema_name,
|
||||
t.name AS table_name,
|
||||
SUM(p.row_count) AS row_count
|
||||
FROM sys.tables t
|
||||
JOIN sys.schemas s ON s.schema_id = t.schema_id
|
||||
JOIN sys.dm_db_partition_stats p ON p.object_id = t.object_id
|
||||
WHERE t.is_ms_shipped = 0
|
||||
AND p.index_id IN (0, 1)
|
||||
GROUP BY s.name, t.name
|
||||
ORDER BY s.name, t.name;
|
||||
|
||||
-- 6. Trigger
|
||||
SELECT
|
||||
s.name AS schema_name,
|
||||
t.name AS table_name,
|
||||
tr.name AS trigger_name,
|
||||
tr.is_disabled,
|
||||
OBJECT_DEFINITION(tr.object_id) AS trigger_definition
|
||||
FROM sys.triggers tr
|
||||
JOIN sys.tables t ON t.object_id = tr.parent_id
|
||||
JOIN sys.schemas s ON s.schema_id = t.schema_id
|
||||
ORDER BY s.name, t.name, tr.name;
|
||||
|
||||
-- 7. Stored Procedures und Funktionen mit Kaffeelisten-Bezug
|
||||
SELECT
|
||||
s.name AS schema_name,
|
||||
o.name AS object_name,
|
||||
o.type_desc,
|
||||
OBJECT_DEFINITION(o.object_id) AS object_definition
|
||||
FROM sys.objects o
|
||||
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
||||
WHERE o.type IN ('P', 'FN', 'IF', 'TF')
|
||||
AND (
|
||||
OBJECT_DEFINITION(o.object_id) LIKE '%kl_%'
|
||||
OR OBJECT_DEFINITION(o.object_id) LIKE '%CoffeeSurvey%'
|
||||
)
|
||||
ORDER BY s.name, o.name;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# M0 Screenshot- und Designreferenz
|
||||
|
||||
Stand: 2026-07-11
|
||||
|
||||
Diese Checkliste friert die bestehende Optik ein, damit die SaaS-App vertraut
|
||||
bleibt. Wenn die Legacy-App lokal nicht laeuft, koennen Screenshots aus der
|
||||
aktuellen Umgebung geliefert werden.
|
||||
|
||||
## Allgemeine Vorgaben
|
||||
|
||||
- Je Seite Desktop mit ca. 1440 px Breite aufnehmen.
|
||||
- Je Kernseite mobile Ansicht mit ca. 390 px Breite aufnehmen.
|
||||
- Je relevanter Rolle aufnehmen: normales Mitglied und Admin.
|
||||
- Keine echten personenbezogenen Daten veroeffentlichen.
|
||||
- Dateinamen nach Muster `m0-<rolle>-<seite>-<viewport>.png`.
|
||||
|
||||
## Kernseiten
|
||||
|
||||
| Prioritaet | Seite | Rolle | Zweck |
|
||||
| --- | --- | --- | --- |
|
||||
| Hoch | `index.php` | Mitglied | Dashboard, Saldo, Jahreswerte, eigene Striche, PayPal |
|
||||
| Hoch | `index.php` | Admin | Dashboard mit Admin-Sidebar |
|
||||
| Hoch | `kaffeeliste.php` | Admin | Gesamtuebersicht und Aktionsleiste |
|
||||
| Hoch | `stricheintragen.php` | Admin | Sammelerfassung Striche |
|
||||
| Hoch | `einzahlung.php` | Admin | Sammelerfassung Einzahlungen |
|
||||
| Hoch | `mitarbeiterverwalten.php` | Admin | Mitgliederliste und Formular |
|
||||
| Hoch | `letzteneintraege.php` | Admin | Korrekturansicht mit Loeschaktionen |
|
||||
| Mittel | `csvupload.php` | Admin | CSV-Upload-Formular und Ergebniszustand |
|
||||
| Mittel | `hinweise.php` | Admin | Hinweise anlegen und Liste |
|
||||
| Mittel | `faq.php` | Mitglied | FAQ-Inhalte und Typografie |
|
||||
| Mittel | `namenanpassen.php` | Mitglied | Formular fuer Anzeigenamen |
|
||||
| Niedrig | `teilnehmerauswertung.php` | Admin | Detailauswertung pro Teilnehmer |
|
||||
| Niedrig | `mailausgebe.php` | Admin | Mail-/Adressausgabe |
|
||||
| Niedrig | `umfrage.php` | Mitglied | Randmodul, falls SaaS-relevant |
|
||||
| Niedrig | `umfrageergebnisse.php` | Admin | Randmodul, falls SaaS-relevant |
|
||||
|
||||
## Zustaende
|
||||
|
||||
Diese UI-Zustaende sollten, wenn moeglich, separat gesichert werden:
|
||||
|
||||
- Positiver Kontostand.
|
||||
- Negativer Kontostand.
|
||||
- Nullsaldo.
|
||||
- Nutzer ohne Zugriff.
|
||||
- Erfolgreiche Stricherfassung.
|
||||
- Erfolgreiche Einzahlung.
|
||||
- CSV-Import mit Erfolg, Dublette und unbekanntem Nutzer.
|
||||
- Hinweisbanner aktiv.
|
||||
- Leere Tabellen oder keine Daten.
|
||||
- Mobile Sidebar geoeffnet und geschlossen.
|
||||
|
||||
## Designmerkmale, die erhalten bleiben sollen
|
||||
|
||||
- Gruener Akzent fuer Links, Buttons und Linien.
|
||||
- Roboto-Slab-Ueberschriften.
|
||||
- Open-Sans-Fliesstext.
|
||||
- Weisse Hauptflaeche.
|
||||
- Graue Sidebar in der App.
|
||||
- Tabellen als zentrale Datenansicht.
|
||||
- Schlichte Formularfelder und Buttons.
|
||||
- Ruhige, arbeitsorientierte Darstellung.
|
||||
|
||||
## Ablage
|
||||
|
||||
Empfohlene lokale Ablage, falls Screenshots ins Repo sollen:
|
||||
|
||||
```text
|
||||
docs/m0/screenshots/
|
||||
```
|
||||
|
||||
Vor dem Commit sollten Screenshots anonymisiert sein. Wenn echte Namen,
|
||||
E-Mails, PayPal-Daten oder Salden sichtbar sind, besser ausserhalb des Repos
|
||||
ablegen und nur als Referenz verwenden.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# M0 Sicherheitsbaseline
|
||||
|
||||
Stand: 2026-07-11
|
||||
|
||||
Diese Baseline ist eine statische Erstbewertung. Sie ersetzt kein Penetration
|
||||
Testing, markiert aber die wichtigsten Risiken fuer die SaaS-Umstrukturierung.
|
||||
|
||||
## Kritische Sofortthemen
|
||||
|
||||
| Risiko | Quelle | Auswirkung | Empfehlung |
|
||||
| --- | --- | --- | --- |
|
||||
| Hart codierte DB-Zugangsdaten | `jahresauswertung.php` Zeilen 4-8 | Secret-Leak, direkte Produktiv-DB-Gefahr | Zugangsdaten rotieren, Skript deaktivieren oder auf Env-Konfiguration umstellen |
|
||||
| Schreibende Seiten ohne eigene Rollenpruefung | `stricheintragen.php`, `einzahlung.php`, `mailversenden.php`, `exportKaffeeliste.php` | Direkter URL-Aufruf kann Aktionen erlauben | Jede Seite serverseitig mit `requireRole` absichern |
|
||||
| Keine CSRF-Token | nahezu alle POST-/Delete-Formulare | Ungewollte Buchungen, Loeschungen, Imports | CSRF fuer alle schreibenden Aktionen |
|
||||
| Harte Deletes fuer Buchungen | `letzteneintraege.php` | Audit-Historie und Revisionsfaehigkeit gehen verloren | Storno-/Reversal-Modell statt Delete |
|
||||
| Upload in Webroot | `csvupload.php` Zeilen 131-138 | Dateiablage kann missbraucht werden | Upload ausserhalb Webroot, Dateityp/MIME/Name pruefen |
|
||||
| Kein Tenant-Scope | alle fachlichen Queries | Zentrales SaaS-Leak-Risiko | `tenant_id` verpflichtend und Query-Schicht testen |
|
||||
|
||||
## Weitere Befunde
|
||||
|
||||
| Befund | Quelle | Empfehlung |
|
||||
| --- | --- | --- |
|
||||
| Direkte SQL-String-Verkettung mit Mailadresse | `functions.php` Zeilen 14, 25, 39, 55 | Parameterisierte Queries |
|
||||
| LDAP-Filter ohne Escaping | `functionsLDAP.php` LDAP-Suchen nach `samaccountname` | LDAP-Filter escapen |
|
||||
| Datumsformat `Y-d-m H:i:s` | `index.php`, `stricheintragen.php`, `einzahlung.php`, `hinweise.php` | ISO/DB-kompatibel `Y-m-d H:i:s` oder DB-Zeit verwenden |
|
||||
| Ausgabe von Namen/E-Mails teils unescaped | mehrere Tabellen, z.B. Mitgliederverwaltung | `htmlspecialchars` zentral erzwingen |
|
||||
| Fehlerausgabe mit `sqlsrv_errors()` an Nutzer | mehrere Dateien | Logging intern, neutrale Fehlermeldung extern |
|
||||
| CSV-Mitarbeitersuche mit mutmasslich falscher Parameteranzahl | `csvupload.php` `getMitarbeiterID` | Query pruefen und mit Tests abdecken |
|
||||
| Basis-Auth-Beispiel mit Platzhalter-Passwort | `umfrageergebnisse.php` Kommentarblock | Entfernen oder echte Auth-Middleware nutzen |
|
||||
| App-Navigation ist in `footer.php` | Layoutstruktur | Trennung in App-Shell und Public-Shell |
|
||||
| `headerline.php` enthaelt NUL-Zeichen | `headerline.php` | Datei pruefen/entfernen, wenn ungenutzt |
|
||||
| Massenmail ohne Versandlog | `mailversenden.php`, `jahresauswertung.php` | Job-Modell mit Dry-Run, Audit, Rate-Limit |
|
||||
|
||||
## Rollen- und Zugriffsrisiken
|
||||
|
||||
Aktuell gilt:
|
||||
|
||||
- Sichtbare Admin-Navigation wird in `footer.php` per `checkKaffeelisteAdmin`
|
||||
gesteuert.
|
||||
- Einige Admin-Zielseiten haben eigene Checks.
|
||||
- Einige schreibende oder sensible Dateien verlassen sich nicht durchgehend auf
|
||||
eine eigene Rollenpruefung.
|
||||
|
||||
Fuer SaaS gilt:
|
||||
|
||||
- Menue-Ausblendung ist keine Berechtigungspruefung.
|
||||
- Jede Route braucht serverseitige Auth- und Rollenpruefung.
|
||||
- Jede fachliche Route braucht Tenant-Kontext.
|
||||
- IDs aus Requests muessen zum aktuellen Tenant gehoeren.
|
||||
|
||||
## Empfohlene Reihenfolge fuer Sicherheitsarbeit
|
||||
|
||||
1. Secrets rotieren und aus dem Code entfernen.
|
||||
2. Legacy-Schreibseiten bis zum Umbau hinter explizite Admin-Pruefung setzen.
|
||||
3. CSRF-Schutz in der neuen App-Basis einplanen.
|
||||
4. Finanzdaten im Zielmodell nur noch stornieren, nicht loeschen.
|
||||
5. Uploads ausserhalb des Webroots modellieren.
|
||||
6. Einheitliche Escape-/View-Helfer einfuehren.
|
||||
7. Tenant-Isolation mit Tests gegen zwei Tenants absichern.
|
||||
|
||||
## Nicht im Repo speichern
|
||||
|
||||
- Produktivpasswoerter.
|
||||
- Echte Nutzerlisten.
|
||||
- Bank-/PayPal-Exportdaten mit Personenbezug.
|
||||
- AD-/LDAP-Service-Account-Daten.
|
||||
- Vollstaendige Produktiv-Dumps.
|
||||
@@ -296,6 +296,15 @@ Uebersicht:
|
||||
Ziel:
|
||||
Den aktuellen Zustand verlaesslich dokumentieren, bevor umgebaut wird.
|
||||
|
||||
Arbeitsartefakte:
|
||||
|
||||
- `docs/m0/README.md`
|
||||
- `docs/m0/code-inventory.md`
|
||||
- `docs/m0/security-baseline.md`
|
||||
- `docs/m0/schema-export.sql`
|
||||
- `docs/m0/golden-master-queries.sql`
|
||||
- `docs/m0/screenshot-checklist.md`
|
||||
|
||||
Schritte:
|
||||
|
||||
- Live-DB-Schema exportieren.
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" ) {
|
||||
$anzahlBetrag = floatval($anzahlBetrag);
|
||||
|
||||
if($anzahlBetrag != 0){
|
||||
$datum = date("Y-d-m H:i:s"); // Das aktuelle Datum verwenden
|
||||
$datum = date("Y-m-d H:i:s"); // Das aktuelle Datum verwenden
|
||||
|
||||
// SQL-Abfrage zum Einfügen der Daten
|
||||
$sql = "INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) VALUES (?, ?, ?)";
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
|
||||
include_once "config.php";
|
||||
|
||||
$devAuthEmail = getenv('DEV_AUTH_EMAIL') ?: '';
|
||||
if ($devAuthEmail !== '') {
|
||||
$mailadress = strtolower(trim($devAuthEmail));
|
||||
$kennung = $mailadress;
|
||||
} else {
|
||||
$kennungtemp = $_SERVER['AUTH_USER'];
|
||||
$teile = explode("\\", $kennungtemp);
|
||||
$kennung = $teile[1];
|
||||
|
||||
|
||||
include_once "functionsLDAP.php";
|
||||
}
|
||||
|
||||
function getUserName($conn, $mail){
|
||||
|
||||
|
||||
Binary file not shown.
+1
-1
@@ -35,7 +35,7 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
|
||||
$dt = DateTime::createFromFormat('Y-m-d\TH:i', $gueltig_bis);
|
||||
|
||||
if ($dt) {
|
||||
$gueltig_bis_sql = $dt->format('Y-d-m H:i:s'); // z.B. "2025-03-09 14:00:00"
|
||||
$gueltig_bis_sql = $dt->format('Y-m-d H:i:s'); // z.B. "2025-09-03 14:00:00"
|
||||
} else {
|
||||
die("Ungültiges Datumsformat");
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ if(checkKaffeelisteAccess($conn, $mailadress)){
|
||||
$anzahlStriche = (int)$anzahlStriche;
|
||||
$kostenproStrich = floatval($KostenproStrichtemp);
|
||||
$kosten = floatval($anzahlStriche * $KostenproStrichtemp);
|
||||
$datum = date("Y-d-m H:i:s"); // Das aktuelle Datum verwenden
|
||||
$datum = date("Y-m-d H:i:s"); // Das aktuelle Datum verwenden
|
||||
if($anzahlStriche != 0){
|
||||
// SQL-Abfrage zum Einfügen der Daten
|
||||
$sql = "INSERT INTO kl_Kaffeeverbrauch (MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum, Eintragsart) VALUES (?, ?, ?, ?, ?, 2)";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
// Einstellungen
|
||||
// Verbindung zur Datenbank herstellen (ersetze die Platzhalter durch deine Daten)
|
||||
$serverName = "sql-nds-autoclient-prod.aoknds.aok";
|
||||
$serverName = "server";
|
||||
$connectionOptions = array(
|
||||
"Database" => "Automatisierungsclient",
|
||||
"Uid" => "ac_admin",
|
||||
"PWD" => "allacc3ssPW",
|
||||
"Database" => "database",
|
||||
"Uid" => "user",
|
||||
"PWD" => "password",
|
||||
"TrustServerCertificate"=>true
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
if (!defined('SQLSRV_FETCH_ASSOC')) {
|
||||
define('SQLSRV_FETCH_ASSOC', 2);
|
||||
}
|
||||
|
||||
if (!defined('SQLSRV_FETCH_NUMERIC')) {
|
||||
define('SQLSRV_FETCH_NUMERIC', 1);
|
||||
}
|
||||
|
||||
final class MysqlSqlsrvConnection
|
||||
{
|
||||
public PDO $pdo;
|
||||
public ?array $lastError = null;
|
||||
|
||||
public function __construct(PDO $pdo)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
}
|
||||
|
||||
final class MysqlSqlsrvStatement
|
||||
{
|
||||
/** @var array<int, array<string, mixed>> */
|
||||
public array $rows;
|
||||
public int $index = 0;
|
||||
public int $affectedRows = 0;
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
*/
|
||||
public function __construct(array $rows = [], int $affectedRows = 0)
|
||||
{
|
||||
$this->rows = $rows;
|
||||
$this->affectedRows = $affectedRows;
|
||||
}
|
||||
}
|
||||
|
||||
function sqlsrv_connect($serverName, array $connectionOptions = [])
|
||||
{
|
||||
$database = getenv('DB_NAME') ?: ($connectionOptions['Database'] ?? '');
|
||||
$user = getenv('DB_USER') ?: ($connectionOptions['Uid'] ?? '');
|
||||
$password = getenv('DB_PASS') ?: ($connectionOptions['PWD'] ?? '');
|
||||
$host = getenv('DB_HOST') ?: (string)$serverName;
|
||||
$port = getenv('DB_PORT') ?: '3306';
|
||||
|
||||
try {
|
||||
$dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $host, $port, $database);
|
||||
$pdo = new PDO($dsn, $user, $password, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => true,
|
||||
]);
|
||||
|
||||
return new MysqlSqlsrvConnection($pdo);
|
||||
} catch (Throwable $e) {
|
||||
$GLOBALS['__sqlsrv_mysql_last_error'] = [[
|
||||
'SQLSTATE' => $e->getCode(),
|
||||
'message' => $e->getMessage(),
|
||||
]];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function sqlsrv_query($conn, string $sql, array $params = [], array $options = [])
|
||||
{
|
||||
if (!$conn instanceof MysqlSqlsrvConnection) {
|
||||
$GLOBALS['__sqlsrv_mysql_last_error'] = [[
|
||||
'SQLSTATE' => 'HY000',
|
||||
'message' => 'Invalid MySQL compatibility connection.',
|
||||
]];
|
||||
return false;
|
||||
}
|
||||
|
||||
$translatedSql = sqlsrv_mysql_translate_query($sql);
|
||||
|
||||
try {
|
||||
$stmt = $conn->pdo->prepare($translatedSql);
|
||||
$stmt->execute(sqlsrv_mysql_normalize_params($translatedSql, $params));
|
||||
|
||||
if ($stmt->columnCount() > 0) {
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($rows as &$row) {
|
||||
$row = sqlsrv_mysql_normalize_row($row);
|
||||
}
|
||||
unset($row);
|
||||
|
||||
return new MysqlSqlsrvStatement($rows, $stmt->rowCount());
|
||||
}
|
||||
|
||||
return new MysqlSqlsrvStatement([], $stmt->rowCount());
|
||||
} catch (Throwable $e) {
|
||||
$conn->lastError = [[
|
||||
'SQLSTATE' => $e->getCode(),
|
||||
'message' => $e->getMessage(),
|
||||
'sql' => $translatedSql,
|
||||
]];
|
||||
$GLOBALS['__sqlsrv_mysql_last_error'] = $conn->lastError;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function sqlsrv_fetch_array($stmt, int $fetchType = SQLSRV_FETCH_ASSOC)
|
||||
{
|
||||
if (!$stmt instanceof MysqlSqlsrvStatement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!array_key_exists($stmt->index, $stmt->rows)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = $stmt->rows[$stmt->index++];
|
||||
|
||||
if ($fetchType === SQLSRV_FETCH_NUMERIC) {
|
||||
return array_values($row);
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
function sqlsrv_num_rows($stmt): int
|
||||
{
|
||||
if (!$stmt instanceof MysqlSqlsrvStatement) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return count($stmt->rows);
|
||||
}
|
||||
|
||||
function sqlsrv_errors()
|
||||
{
|
||||
return $GLOBALS['__sqlsrv_mysql_last_error'] ?? null;
|
||||
}
|
||||
|
||||
function sqlsrv_close($conn): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
function sqlsrv_begin_transaction($conn): bool
|
||||
{
|
||||
return $conn instanceof MysqlSqlsrvConnection && $conn->pdo->beginTransaction();
|
||||
}
|
||||
|
||||
function sqlsrv_commit($conn): bool
|
||||
{
|
||||
return $conn instanceof MysqlSqlsrvConnection && $conn->pdo->commit();
|
||||
}
|
||||
|
||||
function sqlsrv_rollback($conn): bool
|
||||
{
|
||||
return $conn instanceof MysqlSqlsrvConnection && $conn->pdo->rollBack();
|
||||
}
|
||||
|
||||
function sqlsrv_mysql_translate_query(string $sql): string
|
||||
{
|
||||
$query = str_replace(["\r\n", "\r"], "\n", $sql);
|
||||
$query = preg_replace('/\bdbo\./i', '', $query);
|
||||
$query = preg_replace('/\[([A-Za-z0-9_]+)\]/', '`$1`', $query);
|
||||
|
||||
$query = preg_replace('/FORMAT\(([^,]+),\s*\'yyyy\'\)\s*=\s*FORMAT\(GETDATE\(\),\s*\'yyyy\'\)/i', 'YEAR($1) = YEAR(NOW())', $query);
|
||||
$query = preg_replace('/CAST\(GETDATE\(\)\s+AS\s+DATE\)/i', 'CURDATE()', $query);
|
||||
$query = preg_replace('/GETDATE\(\)/i', 'NOW()', $query);
|
||||
$query = preg_replace('/SYSDATETIME\(\)/i', 'NOW()', $query);
|
||||
$query = preg_replace('/CONVERT\(VARCHAR,\s*([^)]+),\s*23\)/i', 'DATE($1)', $query);
|
||||
|
||||
$query = preg_replace(
|
||||
'/DATEADD\(DAY,\s*(-?\d+),\s*\(SELECT\s+MAX\(Datum\)\s+FROM\s+kl_Kaffeeverbrauch\s+WHERE\s+Datum\s+<\s+CURDATE\(\)\)\)/i',
|
||||
'DATE_ADD((SELECT MAX(Datum) FROM kl_Kaffeeverbrauch WHERE Datum < CURDATE()), INTERVAL $1 DAY)',
|
||||
$query
|
||||
);
|
||||
$query = preg_replace(
|
||||
'/DATEADD\(DAY,\s*(-?\d+),\s*\(SELECT\s+MAX\(Datum\)\s+FROM\s+kl_Kaffeeverbrauch\s*\)\)/i',
|
||||
'DATE_ADD((SELECT MAX(Datum) FROM kl_Kaffeeverbrauch), INTERVAL $1 DAY)',
|
||||
$query
|
||||
);
|
||||
$query = preg_replace('/DATEADD\(DAY,\s*(-?\d+),\s*NOW\(\)\)/i', 'DATE_ADD(NOW(), INTERVAL $1 DAY)', $query);
|
||||
|
||||
$limit = null;
|
||||
$query = preg_replace_callback('/^\s*SELECT\s+TOP\s+\(?\s*(\d+)\s*\)?\s+/i', function (array $matches) use (&$limit): string {
|
||||
$limit = (int)$matches[1];
|
||||
return 'SELECT ';
|
||||
}, $query);
|
||||
|
||||
if ($limit !== null && !preg_match('/\bLIMIT\s+\d+\s*$/i', trim($query))) {
|
||||
$query = rtrim($query, " \t\n\r\0\x0B;") . ' LIMIT ' . $limit;
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* PDO is stricter than the SQL Server driver used by the legacy app. Some
|
||||
* legacy calls pass unused parameters, and one CSV lookup passes one value for
|
||||
* two equivalent placeholders. Normalize this only in the dev adapter.
|
||||
*
|
||||
* @param array<int, mixed> $params
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
function sqlsrv_mysql_normalize_params(string $sql, array $params): array
|
||||
{
|
||||
$normalized = array_values($params);
|
||||
$placeholderCount = substr_count($sql, '?');
|
||||
|
||||
if ($placeholderCount === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count($normalized) > $placeholderCount) {
|
||||
return array_slice($normalized, 0, $placeholderCount);
|
||||
}
|
||||
|
||||
if (count($normalized) < $placeholderCount && count($normalized) > 0) {
|
||||
$last = $normalized[count($normalized) - 1];
|
||||
while (count($normalized) < $placeholderCount) {
|
||||
$normalized[] = $last;
|
||||
}
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function sqlsrv_mysql_normalize_row(array $row): array
|
||||
{
|
||||
foreach ($row as $key => $value) {
|
||||
if ($value === null || $value instanceof DateTimeInterface) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_string($value) && preg_match('/(datum|date|gueltig_bis|valid_)/i', (string)$key)) {
|
||||
try {
|
||||
$row[$key] = new DateTime($value);
|
||||
} catch (Throwable $e) {
|
||||
$row[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$required = ['DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASS', 'DEV_AUTH_EMAIL'];
|
||||
foreach ($required as $name) {
|
||||
if ((getenv($name) ?: '') === '') {
|
||||
fwrite(STDERR, "Missing environment variable: {$name}\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4',
|
||||
getenv('DB_HOST'),
|
||||
getenv('DB_PORT') ?: '3306',
|
||||
getenv('DB_NAME')
|
||||
);
|
||||
|
||||
$pdo = new PDO($dsn, getenv('DB_USER'), getenv('DB_PASS'), [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
|
||||
$schema = file_get_contents(__DIR__ . '/../database/mysql-dev-schema.sql');
|
||||
if ($schema === false) {
|
||||
fwrite(STDERR, "Could not read schema file.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$pdo->exec($schema);
|
||||
|
||||
$email = strtolower(trim((string)getenv('DEV_AUTH_EMAIL')));
|
||||
$name = trim((string)(getenv('DEV_AUTH_NAME') ?: 'Test Admin'));
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO kl_Mitarbeiter (Name, Email, aktiv, admin)
|
||||
VALUES (?, ?, 1, 1)
|
||||
ON DUPLICATE KEY UPDATE Name = VALUES(Name), aktiv = 1, admin = 1'
|
||||
);
|
||||
$stmt->execute([$name, $email]);
|
||||
|
||||
$memberId = (int)$pdo->query('SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email = ' . $pdo->quote($email))->fetchColumn();
|
||||
|
||||
$paymentCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_Einzahlungen WHERE MitarbeiterID = ' . $memberId)->fetchColumn();
|
||||
if ($paymentCount === 0) {
|
||||
$stmt = $pdo->prepare('INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) VALUES (?, ?, NOW())');
|
||||
$stmt->execute([$memberId, 10.00]);
|
||||
}
|
||||
|
||||
$consumptionCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_Kaffeeverbrauch WHERE MitarbeiterID = ' . $memberId)->fetchColumn();
|
||||
if ($consumptionCount === 0) {
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO kl_Kaffeeverbrauch (MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum, Eintragsart)
|
||||
VALUES (?, ?, ?, ?, NOW(), ?)'
|
||||
);
|
||||
$stmt->execute([$memberId, 3, 0.60, 0.20, 2]);
|
||||
}
|
||||
|
||||
$noticeCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_hinweise')->fetchColumn();
|
||||
if ($noticeCount === 0) {
|
||||
$stmt = $pdo->prepare('INSERT INTO kl_hinweise (nachricht, gueltig_bis) VALUES (?, DATE_ADD(NOW(), INTERVAL 30 DAY))');
|
||||
$stmt->execute(['Dev-Hinweis: MySQL-Testumgebung aktiv.']);
|
||||
}
|
||||
|
||||
echo "MySQL dev schema is ready for {$email}.\n";
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PHP_BIN="${PHP_BIN:-$ROOT_DIR/.local/php/usr/bin/php8.3}"
|
||||
PHP_INI="${PHP_INI:-$ROOT_DIR/.local/php-dev.ini}"
|
||||
PHP_LIB_DIR="$ROOT_DIR/.local/php/usr/lib/x86_64-linux-gnu"
|
||||
|
||||
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-$PHP_LIB_DIR}"
|
||||
export APP_DB_DRIVER="${APP_DB_DRIVER:-mysql}"
|
||||
export APP_ENV="${APP_ENV:-dev}"
|
||||
export DEV_AUTH_EMAIL="${DEV_AUTH_EMAIL:-admin@test.local}"
|
||||
export DEV_AUTH_NAME="${DEV_AUTH_NAME:-Test Admin}"
|
||||
|
||||
exec "$PHP_BIN" -c "$PHP_INI" -S "${HOST:-127.0.0.1}:${PORT:-8080}" -t "$ROOT_DIR"
|
||||
+1
-1
@@ -31,7 +31,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
$anzahlStriche = (int)$anzahlStriche;
|
||||
$kostenproStrich = floatval($kostenproStrich);
|
||||
$kosten = floatval($anzahlStriche * $kostenproStrich);
|
||||
$datum = date("Y-d-m H:i:s"); // Das aktuelle Datum verwenden
|
||||
$datum = date("Y-m-d H:i:s"); // Das aktuelle Datum verwenden
|
||||
if($anzahlStriche != 0){
|
||||
// SQL-Abfrage zum Einfügen der Daten
|
||||
$sql = "INSERT INTO kl_Kaffeeverbrauch (MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum) VALUES (?, ?, ?, ?, ?)";
|
||||
|
||||
Reference in New Issue
Block a user