From de3dcee7ec605873c87ae725c37844806334f993 Mon Sep 17 00:00:00 2001 From: Clemens Creutzburg Date: Tue, 14 Jul 2026 23:49:33 +0200 Subject: [PATCH] M5: Mitgliederverwaltung tenant- und rollenbasiert umstellen - mitarbeiterverwalten.php war rein ueber Legacy-Admin gesperrt und haette neue SaaS-Mandanten ausgeschlossen; jetzt Rollen-/Legacy-Fallback wie in kaffeeliste.php (owner/admin). - Anlegen/Bearbeiten/Aktivieren/Deaktivieren spiegeln transaktional nach participants (ledger_mirror_legacy_participant neu ergaenzt), damit Ledger-Ansichten sofort den aktuellen Mitgliederstand zeigen. - Gespeicherte XSS-Luecke behoben: Name/E-Mail waren in Formular und Liste ungeschuetzt ausgegeben, jetzt ueber saas_html(). - Admin-Rollenvergabe bleibt bewusst Legacy-only (kein automatischer Login-Account ohne Einladungsflow); dokumentiert als offener Punkt. Co-Authored-By: Claude Sonnet 5 --- app/ledger.php | 30 ++++++++++++ docs/m5-app-kern.md | 47 +++++++++++++++++- mitarbeiterverwalten.php | 102 ++++++++++++++++++++++++--------------- 3 files changed, 138 insertions(+), 41 deletions(-) diff --git a/app/ledger.php b/app/ledger.php index e6d8b86..1593b33 100644 --- a/app/ledger.php +++ b/app/ledger.php @@ -319,6 +319,36 @@ function ledger_mirror_legacy_consumption(PDO $pdo, int $tenantId, int $legacyCo } } +/** + * Mirrors a single kl_Mitarbeiter row into participants, so member changes + * made through the legacy admin UI are immediately visible to ledger-backed + * reads instead of waiting for the next scripts/backfill-default-tenant.php run. + */ +function ledger_mirror_legacy_participant(PDO $pdo, int $tenantId, int $legacyMitarbeiterId): void +{ + $stmt = $pdo->prepare( + "INSERT INTO participants + (tenant_id, display_name, email, email_norm, paypal_name, active, legacy_mitarbeiter_id) + SELECT + ?, + NULLIF(TRIM(m.Name), ''), + NULLIF(TRIM(m.Email), ''), + NULLIF(LOWER(TRIM(m.Email)), ''), + NULLIF(TRIM(m.paypalname), ''), + m.aktiv, + m.MitarbeiterID + FROM kl_Mitarbeiter m + WHERE m.MitarbeiterID = ? + ON DUPLICATE KEY UPDATE + display_name = COALESCE(VALUES(display_name), participants.display_name), + email = VALUES(email), + email_norm = VALUES(email_norm), + paypal_name = VALUES(paypal_name), + active = VALUES(active)" + ); + $stmt->execute([$tenantId, $legacyMitarbeiterId]); +} + function ledger_mirror_legacy_payment(PDO $pdo, int $tenantId, int $legacyPaymentId): void { $stmt = $pdo->prepare( diff --git a/docs/m5-app-kern.md b/docs/m5-app-kern.md index caafb00..e31e671 100644 --- a/docs/m5-app-kern.md +++ b/docs/m5-app-kern.md @@ -127,12 +127,52 @@ Alle drei Flows wurden gegen die Remote-Dev-Datenbank live per HTTP getestet `letzteneintraege.php`) und anschließend wieder auf den Ausgangsstand zurückgesetzt. +## Fortsetzung: Mitgliederverwaltung + +Umgesetzte Dateien: + +```text +mitarbeiterverwalten.php +app/ledger.php +``` + +Umfang: + +- Zugriff nutzte bisher ausschließlich `checkKaffeelisteAdmin` und hätte neue + SaaS-Mandanten ohne Legacy-Mitarbeiterzeile ausgeschlossen. Jetzt gilt die + gleiche Rollen-/Fallback-Logik wie in `kaffeeliste.php`, mit den Rollen + `owner` und `admin` (nicht `treasurer`, da Mitgliederpflege sensibler ist + als reine Zahlungsvorgänge). +- Anlegen, Bearbeiten, Aktivieren und Deaktivieren schreiben weiterhin zuerst + in `kl_Mitarbeiter` und spiegeln danach in derselben Transaktion über die + neue Funktion `ledger_mirror_legacy_participant()` nach `participants`. + Ledger-Ansichten (Dashboard, Kaffeeliste, Teilnehmerauswertung) sehen neue + oder geänderte Mitglieder damit sofort, ohne auf einen manuellen Lauf von + `scripts/backfill-default-tenant.php` zu warten. +- Eine gespeicherte-XSS-Lücke wurde behoben: Name und E-Mail wurden beim + Bearbeiten-Formular und in der Mitgliederliste bisher ungeschützt + ausgegeben (nur `paypalname` war escaped). Beide Stellen nutzen jetzt + `saas_html()`. +- Live gegen die Dev-Datenbank getestet: Anlegen mit einem + HTML/Skript-Payload im Namen (korrekt escaped in der Ausgabe, korrekt + gespiegelt in `participants`), Deaktivieren (spiegelt `active = 0`). + +Bewusst nicht umgesetzt: + +- Der Haken "Administrator" bleibt ein reines Legacy-Feld auf + `kl_Mitarbeiter.admin` und wird nicht automatisch in eine + `tenant_memberships`-Rolle übersetzt. Das würde einen Login-Account ohne + Einladung/Passwort-Setzung anlegen, was ein eigenes, sauber zu + bauendes Einladungs-Flow braucht (E-Mail-Versand, Token, Passwortsetzung). + Admin-Rollen für neue SaaS-Nutzer laufen bis dahin weiter über + `scripts/backfill-default-tenant.php` oder die Registrierung. + ## Noch offen - Eigene PayPal-/Zahlungsbereich als eigenständiger App-Screen (aktuell nur im Dashboard integriert). -- Mitgliederverwaltung tenant- und rollenbasiert umsetzen - (`mitarbeiterverwalten.php` ist noch nicht angefasst). +- Einladungs-Flow, um bestehenden Teilnehmern nachträglich einen + Login-Account mit `tenant_memberships`-Rolle zuzuweisen. - Hinweise als tenant-spezifische Notices umsetzen. - Export, Mail und Jahresprozesse bleiben M6-Themen. @@ -144,3 +184,6 @@ zurückgesetzt. - HTTP-Smoke: grün mit 23 geprüften Seiten. - Live-Test der Schreibflows gegen die Dev-DB: Sammelstriche, Sammeleinzahlung und Storno beider Buchungsarten erfolgreich geprüft. +- Live-Test der Mitgliederverwaltung gegen die Dev-DB: Anlegen (inklusive + XSS-Payload-Check), Deaktivieren, participants-Spiegelung erfolgreich + geprüft. diff --git a/mitarbeiterverwalten.php b/mitarbeiterverwalten.php index 3c18dc4..cca06fa 100644 --- a/mitarbeiterverwalten.php +++ b/mitarbeiterverwalten.php @@ -2,6 +2,7 @@ include "functions.php"; +require_once __DIR__ . "/app/ledger.php"; app_require_csrf(); include "header.php"; include "headerline.php"; @@ -17,66 +18,89 @@ include "nav.php"; beginTransaction(); - // Füge die MitarbeiterID nur bei Bearbeitung hinzu - if ($aktion === 'bearbeitenspeichern' ) { - array_push($params, $mitgliedID); - }elseif($aktion === 'aktivieren' || $aktion === 'deaktivieren'){ - $params = array($mitgliedID); - } + $stmt = $pdo->prepare($sql); + $stmt->execute($params); - $stmt = sqlsrv_query($conn, $sql, $params); + $legacyMitarbeiterId = $aktion === 'anlegen' ? (int)$pdo->lastInsertId() : (int)$mitgliedID; + ledger_mirror_legacy_participant($pdo, $tenantId, $legacyMitarbeiterId); - if ($stmt === false) { - throw new Exception(print_r(sqlsrv_errors(), true)); - } + $pdo->commit(); return true; // Erfolgreich - } catch (Exception $e) { - return $e->getMessage(); // Fehlermeldung zurückgeben + } catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + + return 'Die Aktion konnte nicht gespeichert werden.'; } }?> prepare("SELECT * FROM kl_Mitarbeiter WHERE MitarbeiterID = ?"); + $stmtEinzelmitglied->execute([$mitgliedID]); + $einzelmitglied = $stmtEinzelmitglied->fetch(); ?> -

Bearbeiten von

+

Bearbeiten von

"> - + - - + + - + @@ -101,7 +125,7 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){ $aktion = $_POST["aktion"]; if ($aktion === 'anlegen' || $aktion === 'bearbeitenspeichern') { - $mitgliedID = isset($_POST["mitgliedID"]) ? $_POST["mitgliedID"] : null; + $mitgliedID = isset($_POST["mitgliedID"]) ? (int)$_POST["mitgliedID"] : null; $name = $_POST["name"]; $email = $_POST["email"]; $paypalname = trim($_POST["paypalname"] ?? ''); @@ -109,7 +133,7 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){ $aktiv = isset($_POST["aktiv"]) ? 1 : 0; $admin = isset($_POST["admin"]) ? 1 : 0; - $ergebnis = bearbeiteMitglied($aktion, $mitgliedID, $name, $email, $paypalname, $aktiv, $admin, $conn); + $ergebnis = bearbeiteMitglied($aktion, $mitgliedID, $name, $email, $paypalname, $aktiv, $admin, $pdo, $tenantId); if ($ergebnis === true) { echo "Aktion erfolgreich durchgeführt."; @@ -117,9 +141,9 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){ echo "Fehler: $ergebnis"; } } elseif ($aktion === 'aktivieren') { - $mitgliedID = $_POST["mitgliedID"]; + $mitgliedID = (int)$_POST["mitgliedID"]; - $ergebnis = bearbeiteMitglied('aktivieren', $mitgliedID, null, null, null, null, null, $conn); + $ergebnis = bearbeiteMitglied('aktivieren', $mitgliedID, null, null, null, null, null, $pdo, $tenantId); if ($ergebnis === true) { echo "Mitglied erfolgreich aktiviert."; @@ -127,9 +151,9 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){ echo "Fehler: $ergebnis"; } } elseif ($aktion === 'deaktivieren') { - $mitgliedID = $_POST["mitgliedID"]; + $mitgliedID = (int)$_POST["mitgliedID"]; - $ergebnis = bearbeiteMitglied('deaktivieren', $mitgliedID, null, null, null, null, null, $conn); + $ergebnis = bearbeiteMitglied('deaktivieren', $mitgliedID, null, null, null, null, null, $pdo, $tenantId); if ($ergebnis === true) { echo "Mitglied erfolgreich deaktiviert."; @@ -141,8 +165,7 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){ // Mitglieder aus der Datenbank abrufen - $sqlMitglieder = "SELECT MitarbeiterID, Name, Email, paypalname, aktiv, admin FROM kl_Mitarbeiter ORDER BY Name"; - $stmtMitglieder = sqlsrv_query($conn, $sqlMitglieder); + $stmtMitglieder = $pdo->query("SELECT MitarbeiterID, Name, Email, paypalname, aktiv, admin FROM kl_Mitarbeiter ORDER BY Name"); ?> @@ -197,13 +220,14 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){ fetch()) { + $mitgliedIdRow = (int)$row['MitarbeiterID']; + echo ''; - echo "{$row['MitarbeiterID']}"; - echo "{$row['Name']}"; - echo "{$row['Email']}"; - echo "" . htmlspecialchars($row['paypalname'] ?? '', ENT_QUOTES, 'UTF-8') . ""; + echo "{$mitgliedIdRow}"; + echo "" . saas_html($row['Name']) . ""; + echo "" . saas_html($row['Email']) . ""; + echo "" . saas_html($row['paypalname'] ?? '') . ""; echo "{$row['aktiv']}"; echo "{$row['admin']}"; echo "";