Die Zugriffspruefung lief bisher in 19 Dateien auf einen Fallback gegen die Legacy-Tabelle kl_Mitarbeiter (checkKaffeelisteAdmin/-Access). Der Zugang haengt jetzt ausschliesslich an der SaaS-Anmeldung. - Fallback-Bloecke in allen 19 Dateien entfernt, ebenso getUserName/getUserId - functions.php besteht nur noch aus der SaaS-Anmeldung; die per String zusammengebaute Abfrage "WHERE Email like '<eingabe>'" ist damit weg - config.php baut keine sqlsrv-Verbindung mehr auf, der Kompatibilitaetslayer lib/sqlsrv_mysql_compat.php ist verwaist und entfaellt - Auto-Login per DEV_AUTH_EMAIL gibt es nicht mehr: er meldete jeden Besucher an und machte damit den Login-Schutz unpruefbar. Angemeldet wird auch lokal ueber login.php; die Variable dient nur noch scripts/init-mysql-dev.php http-smoke war an die Golden-Master-Daten des Default-Mandanten gebunden und hatte deshalb sechs dauerhaft rote Pruefungen. Der Test legt sich jetzt einen eigenen Mandanten mit bekannten Salden an, meldet sich per HTTP an und raeumt danach auf. Geschuetzte Seiten werden nicht mehr ueber das Wort "Login" im Text geprueft, sondern ueber die tatsaechliche 302-Umleitung. http-smoke 27 PASS / 6 FAIL -> 33 PASS / 0 FAIL role-matrix 55 PASS / 0 FAIL -> unveraendert tenant-isolation 9 PASS / 1 FAIL -> 11 PASS / 1 FAIL (Altfehler) Ausserdem "keine Zugang" -> "keinen Zugang" in zwei Fehlermeldungen; der Rollentest erkannte die Ablehnung an genau diesem Tippfehler und wurde mitgezogen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
169 lines
5.4 KiB
PHP
169 lines
5.4 KiB
PHP
<?php
|
|
|
|
include "functions.php";
|
|
require_once __DIR__ . "/app/ledger.php";
|
|
require_once __DIR__ . "/app/saas-mail.php";
|
|
require_once __DIR__ . "/app/audit.php";
|
|
app_require_csrf();
|
|
|
|
$pdo = app_db_pdo();
|
|
$saasUser = saas_current_user($pdo);
|
|
$tenantId = 0;
|
|
$hasAccess = false;
|
|
|
|
if ($saasUser !== null && saas_user_has_role(['owner', 'admin', 'treasurer'], $saasUser)) {
|
|
$tenantId = (int)$saasUser['tenant_id'];
|
|
$hasAccess = true;
|
|
}
|
|
|
|
$ergebnisse = null;
|
|
$dryRun = true;
|
|
$liveFlash = false;
|
|
|
|
// Ergebnis eines vorangegangenen Live-Versands (Post-Redirect-Get): nach dem
|
|
// echten Versand wird auf diese Seite zurueckgeleitet, damit ein Refresh nicht
|
|
// erneut an alle Mitglieder mailt.
|
|
if ($hasAccess && isset($_SESSION['flash_mailversand'])) {
|
|
$flash = $_SESSION['flash_mailversand'];
|
|
unset($_SESSION['flash_mailversand']);
|
|
if ((int)($flash['tenant_id'] ?? 0) === $tenantId) {
|
|
$ergebnisse = $flash['ergebnisse'] ?? null;
|
|
$dryRun = false;
|
|
$liveFlash = true;
|
|
}
|
|
}
|
|
|
|
if ($hasAccess && $_SERVER["REQUEST_METHOD"] === "POST") {
|
|
$dryRun = !empty($_POST['dry_run']);
|
|
$settings = saas_fetch_tenant_settings($pdo, $tenantId);
|
|
$dashboardUrl = saas_app_url('index.php');
|
|
$createdByUserId = $saasUser['user_id'] ?? null;
|
|
|
|
$teilnehmer = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]);
|
|
$ergebnisse = [];
|
|
|
|
foreach ($teilnehmer as $person) {
|
|
$email = trim((string)($person['email'] ?? ''));
|
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
$ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'ohne_email'];
|
|
continue;
|
|
}
|
|
|
|
$subject = 'Kaffeeliste - Dein aktueller Stand';
|
|
$body = saas_render_balance_mail_body(
|
|
$person['display_name'],
|
|
(int)$person['balance_cents'],
|
|
$settings,
|
|
$dashboardUrl
|
|
);
|
|
|
|
if ($dryRun) {
|
|
saas_log_outbound_email($pdo, $tenantId, $person['participant_id'], 'balance_notice', $subject, 'dry_run', null, $createdByUserId);
|
|
$ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'dry_run'];
|
|
continue;
|
|
}
|
|
|
|
$sendResult = saas_send_mail($email, $subject, $body);
|
|
if ($sendResult['ok']) {
|
|
saas_log_outbound_email($pdo, $tenantId, $person['participant_id'], 'balance_notice', $subject, 'sent', null, $createdByUserId);
|
|
$ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'sent'];
|
|
} else {
|
|
saas_log_outbound_email($pdo, $tenantId, $person['participant_id'], 'balance_notice', $subject, 'failed', $sendResult['error'] ?? 'unbekannter Fehler', $createdByUserId);
|
|
$ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'failed'];
|
|
}
|
|
}
|
|
|
|
if (!$dryRun) {
|
|
app_audit_log($pdo, $tenantId, $createdByUserId, 'mail_broadcast.sent', 'tenant', $tenantId, ['empfaenger' => count($ergebnisse)]);
|
|
|
|
// Post-Redirect-Get: Ergebnis in die Session legen und per GET erneut
|
|
// anzeigen, damit ein Browser-Refresh nicht erneut versendet.
|
|
$_SESSION['flash_mailversand'] = ['tenant_id' => $tenantId, 'ergebnisse' => $ergebnisse];
|
|
header('Location: mailversenden.php');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
$log = $hasAccess ? saas_fetch_outbound_email_log($pdo, $tenantId, 20) : [];
|
|
|
|
function mailversenden_status_label(string $status): string
|
|
{
|
|
return match ($status) {
|
|
'sent' => 'Versendet',
|
|
'failed' => 'Fehlgeschlagen',
|
|
'dry_run' => 'Dry-Run (nicht versendet)',
|
|
'ohne_email' => 'Keine E-Mail-Adresse',
|
|
default => $status,
|
|
};
|
|
}
|
|
|
|
include "header.php";
|
|
include "headerline.php";
|
|
include "nav.php";
|
|
?>
|
|
|
|
|
|
<!-- Banner -->
|
|
<section id="banner">
|
|
<div class="content">
|
|
|
|
<?php if (!$hasAccess): ?>
|
|
<h2>Kein Zugriff</h2>
|
|
<?php else: ?>
|
|
|
|
<h2>Info-Mail versenden</h2>
|
|
<p>Verschickt an alle aktiven Mitglieder eine E-Mail mit dem aktuellen
|
|
Kaffeelisten-Stand. Im Dry-Run wird nichts wirklich versendet, aber jeder
|
|
Empfänger wird im Versandlog unten protokolliert.</p>
|
|
|
|
<?php if ($liveFlash): ?>
|
|
<div class="hint-box success"><p>Die Info-Mails wurden versendet.</p></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($ergebnisse !== null): ?>
|
|
<h3>Ergebnis dieses Laufs (<?php echo $dryRun ? 'Dry-Run' : 'Live-Versand'; ?>)</h3>
|
|
<table>
|
|
<tr><th>Name</th><th>E-Mail</th><th>Status</th></tr>
|
|
<?php foreach ($ergebnisse as $eintrag): ?>
|
|
<tr>
|
|
<td><?php echo saas_html($eintrag['name']); ?></td>
|
|
<td><?php echo saas_html($eintrag['email']); ?></td>
|
|
<td><?php echo saas_html(mailversenden_status_label($eintrag['status'])); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</table>
|
|
<?php endif; ?>
|
|
|
|
<form method="post" action="mailversenden.php">
|
|
<?php echo app_csrf_field(); ?>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="dry_run" id="dry_run" checked>
|
|
<label class="form-check-label" for="dry_run">Dry-Run (nichts wirklich versenden, nur protokollieren)</label>
|
|
</div>
|
|
<button type="submit">Info-Mail versenden</button>
|
|
</form>
|
|
|
|
<h3>Versandlog (letzte 20)</h3>
|
|
<table>
|
|
<tr><th>Datum</th><th>Mitglied</th><th>Betreff</th><th>Status</th><th>Fehler</th></tr>
|
|
<?php if ($log === []): ?>
|
|
<tr><td colspan="5">Noch kein Versand protokolliert.</td></tr>
|
|
<?php endif; ?>
|
|
<?php foreach ($log as $eintrag): ?>
|
|
<tr>
|
|
<td><?php echo saas_html($eintrag['created_at']); ?></td>
|
|
<td><?php echo saas_html($eintrag['display_name'] ?? '—'); ?></td>
|
|
<td><?php echo saas_html($eintrag['subject']); ?></td>
|
|
<td><?php echo saas_html(mailversenden_status_label($eintrag['status'])); ?></td>
|
|
<td><?php echo saas_html($eintrag['error'] ?? ''); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</table>
|
|
|
|
<?php endif; ?>
|
|
|
|
</div>
|
|
</section>
|
|
|
|
<?php include "footer.php"; ?>
|