Sicherheits-/Korrektheits-Fixes und neue Funktionen
Bugfixes aus dem Code-Review: - XSS: unescaptes $_SERVER['PHP_SELF'] in csvupload.php und letzteneintraege.php durch feste Seitennamen ersetzt. - Stripe-Webhook: Event-Deduplizierung (neue Tabelle stripe_webhook_events) gegen doppelte Verarbeitung/Dolibarr-Rechnungen bei Retry-Zustellung. - Stripe-Webhook: Tarifwechsel aus dem Kundenportal wird lokal nachgezogen (plan_code aus dem Preis-lookup_key bei subscription.updated). - Post-Redirect-Get fuer jahresauswertung, mailversenden und die Selbst-Stricheintragung - verhindert Doppelbuchung/Doppelversand per Browser-Refresh. - Jahresbonus: Mails erst nach erfolgreichem Commit; Restcent-Ausgleich beim letzten Empfaenger, damit die Summe exakt stimmt. - Verschachtelte HTML-Dokumente in csvupload/einzahlung/stricheintragen entfernt (Layout kommt aus header.php). - Rate-Limit fuer den Versand von E-Mail-Verifizierungslinks. Neue Funktionen: - Mitglieder koennen ihren zuletzt selbst eingetragenen Strich wieder stornieren (nur eigene Web-Eintraege, Kassenwart-Eintraege bleiben). - Monatsuebersicht des eigenen Verbrauchs im Mitglieder-Dashboard. - Automatische Zahlungserinnerung: opt-in pro Mandant ab der Warnschwelle, mit Intervall; Cron-Skript scripts/send-payment-reminders.php. - CSV-Import fuer Mitgliederlisten inkl. herunterladbarer Vorlage (mitglieder-vorlage.php), Semikolon-/Komma- und BOM-Erkennung. - Logo als PDF-Wasserzeichen pro Mandant (Upload in den Mandant- Einstellungen, geschuetzt unter var/tenant_logos/, ersetzt den Text-Wasserzeichen im Ausdruck). Robusterer Teilnehmer-Lookup im Dashboard ueber user_id (Fallback E-Mail).
This commit is contained in:
@@ -6,6 +6,7 @@ require_once __DIR__ . "/app/ledger.php";
|
||||
require_once __DIR__ . "/app/saas-mail.php";
|
||||
require_once __DIR__ . "/app/audit.php";
|
||||
require_once __DIR__ . "/app/billing.php";
|
||||
require_once __DIR__ . "/app/imports.php";
|
||||
app_require_csrf();
|
||||
include "header.php";
|
||||
include "headerline.php";
|
||||
@@ -155,9 +156,60 @@ if($hasAccess){
|
||||
} else {
|
||||
$fehler = 'Das Mitglied konnte nicht anonymisiert werden.';
|
||||
}
|
||||
} elseif ($aktion === 'csv_import') {
|
||||
$file = $_FILES['mitglieder_csv'] ?? null;
|
||||
if ($file === null || ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
$fehler = 'Bitte eine CSV-Datei auswählen.';
|
||||
} elseif (($file['size'] ?? 0) <= 0 || $file['size'] > 2 * 1024 * 1024) {
|
||||
$fehler = 'Die CSV-Datei ist leer oder größer als 2 MB.';
|
||||
} elseif (strtolower(pathinfo((string)$file['name'], PATHINFO_EXTENSION)) !== 'csv') {
|
||||
$fehler = 'Es sind nur CSV-Dateien erlaubt.';
|
||||
} elseif (!is_uploaded_file((string)$file['tmp_name'])) {
|
||||
$fehler = 'Die hochgeladene Datei konnte nicht verifiziert werden.';
|
||||
} else {
|
||||
try {
|
||||
$zeilen = imports_parse_member_csv((string)$file['tmp_name']);
|
||||
$angelegt = 0;
|
||||
$uebersprungen = 0;
|
||||
$inaktivWegenKapazitaet = 0;
|
||||
$fehlerhaft = 0;
|
||||
foreach ($zeilen as $z) {
|
||||
if ($z['name'] === '' || !filter_var($z['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
$fehlerhaft++;
|
||||
continue;
|
||||
}
|
||||
$aktiv = $z['active'];
|
||||
if ($aktiv && !billing_has_capacity_for($pdo, $tenantId)) {
|
||||
$aktiv = false;
|
||||
$inaktivWegenKapazitaet++;
|
||||
}
|
||||
try {
|
||||
$neueId = ledger_create_participant($pdo, $tenantId, $z['name'], $z['email'], $z['paypal_name'] !== '' ? $z['paypal_name'] : null, $aktiv);
|
||||
app_audit_log($pdo, $tenantId, $actorUserId, 'participant.created', 'participant', $neueId, ['name' => $z['name'], 'email' => $z['email'], 'quelle' => 'csv_import']);
|
||||
$angelegt++;
|
||||
} catch (Throwable $e) {
|
||||
$uebersprungen++;
|
||||
}
|
||||
}
|
||||
$teile = ["{$angelegt} angelegt"];
|
||||
if ($uebersprungen > 0) {
|
||||
$teile[] = "{$uebersprungen} übersprungen (bereits vorhanden)";
|
||||
}
|
||||
if ($inaktivWegenKapazitaet > 0) {
|
||||
$teile[] = "{$inaktivWegenKapazitaet} als inaktiv angelegt (Tarif-Kapazität erschöpft)";
|
||||
}
|
||||
if ($fehlerhaft > 0) {
|
||||
$teile[] = "{$fehlerhaft} fehlerhaft (Name/E-Mail ungültig)";
|
||||
}
|
||||
$meldung = 'CSV-Import: ' . implode(', ', $teile) . '.';
|
||||
} catch (Throwable $e) {
|
||||
$fehler = 'Die CSV-Datei konnte nicht verarbeitet werden.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$mitglieder = ledger_fetch_participants_for_admin($pdo, $tenantId);
|
||||
$billingInfo = billing_fetch_or_init($pdo, $tenantId);
|
||||
$billingPlan = billing_plans()[$billingInfo['plan_code']] ?? null;
|
||||
@@ -246,6 +298,20 @@ if($hasAccess){
|
||||
<button type="submit">Mitglied anlegen</button>
|
||||
</form>
|
||||
|
||||
<h3>Mitglieder per CSV importieren</h3>
|
||||
<p>Lade zunächst die Vorlage herunter, trage eure Mitglieder ein (Name, E-Mail,
|
||||
optional PayPal-Name und „ja/nein" für aktiv) und lade die Datei anschließend
|
||||
wieder hoch. Bereits vorhandene E-Mail-Adressen werden übersprungen.</p>
|
||||
<p><a class="button" href="mitglieder-vorlage.php">Vorlage herunterladen</a></p>
|
||||
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" enctype="multipart/form-data">
|
||||
<?php echo app_csrf_field(); ?>
|
||||
<input type="hidden" name="aktion" value="csv_import">
|
||||
<label for="mitglieder_csv">CSV-Datei mit Mitgliedern:</label>
|
||||
<input type="file" name="mitglieder_csv" id="mitglieder_csv" accept=".csv" required>
|
||||
<button type="submit">Mitglieder importieren</button>
|
||||
</form>
|
||||
|
||||
|
||||
<p>Name und E-Mail dienen der Kaffeeliste und Benachrichtigungen. Ein Login-Zugang
|
||||
ist davon unabhängig und wird separat je Mitglied gewährt oder entzogen.</p>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user