Files
clemensandClaude Opus 4.8 3e24910831 Legacy-Abbau Schritt 2: Legacy-Authentifizierung entfernt
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>
2026-07-21 21:10:09 +02:00

247 lines
7.3 KiB
PHP

<?php
include "functions.php";
require_once __DIR__ . "/app/ledger.php";
require_once __DIR__ . "/app/imports.php";
require_once __DIR__ . "/app/audit.php";
app_require_csrf();
include "header.php";
include "headerline.php";
include "nav.php";
?>
<!-- Banner -->
<section id="banner">
<div class="content">
<?php
$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;
}
if (!$hasAccess) {
echo "<h2>Kein Zugriff</h2>";
include "footer.php";
exit;
}
function csv_upload_dir(): string
{
return APP_ROOT . '/var/uploads';
}
/**
* @return array{0: ?string, 1: ?string} [Pfad, Fehlermeldung]
*/
function csv_prepare_upload(array $file): array
{
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
return [null, 'Fehler beim Hochladen der Datei.'];
}
if (($file['size'] ?? 0) <= 0 || $file['size'] > 5 * 1024 * 1024) {
return [null, 'Die CSV-Datei ist leer oder größer als 5 MB.'];
}
$originalName = (string)($file['name'] ?? '');
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if ($extension !== 'csv') {
return [null, 'Es sind nur CSV-Dateien erlaubt.'];
}
$tmpName = (string)($file['tmp_name'] ?? '');
if (!is_uploaded_file($tmpName)) {
return [null, 'Die hochgeladene Datei konnte nicht verifiziert werden.'];
}
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = $finfo ? finfo_file($finfo, $tmpName) : false;
if ($finfo) {
finfo_close($finfo);
}
$allowedMimeTypes = [
'text/plain', 'text/csv', 'text/x-csv', 'application/csv',
'application/vnd.ms-excel', 'application/octet-stream',
];
if (is_string($mime) && !in_array($mime, $allowedMimeTypes, true)) {
return [null, 'Der Dateityp wurde nicht als CSV erkannt.'];
}
}
$uploadDir = csv_upload_dir();
if (!is_dir($uploadDir)) {
@mkdir($uploadDir, 0700, true);
}
if (!is_dir($uploadDir) || !is_writable($uploadDir)) {
return [null, 'Der Upload-Ordner ist nicht beschreibbar.'];
}
$targetFile = $uploadDir . '/paypal_' . date('Ymd_His') . '_' . bin2hex(random_bytes(8)) . '.csv';
if (!move_uploaded_file($tmpName, $targetFile)) {
return [null, 'Die CSV-Datei konnte nicht gespeichert werden.'];
}
return [$targetFile, null];
}
function csv_status_label(string $status): string
{
return match ($status) {
'matched' => 'Wird importiert',
'duplicate' => 'Bereits vorhanden',
'unmatched' => 'Mitglied nicht gefunden',
'invalid' => 'Ungültige Zeile',
'imported' => 'Importiert',
default => $status,
};
}
$meldung = null;
$fehler = null;
$vorschauBatchId = null;
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$aktion = $_POST['aktion'] ?? 'hochladen';
if ($aktion === 'hochladen' && isset($_FILES["csv_file"])) {
[$csvFile, $uploadError] = csv_prepare_upload($_FILES["csv_file"]);
if ($uploadError !== null) {
$fehler = $uploadError;
} else {
try {
$checksum = hash_file('sha256', $csvFile);
$rows = imports_parse_csv($csvFile);
$pdo->beginTransaction();
$batchId = imports_create_batch(
$pdo,
$tenantId,
basename((string)$_FILES["csv_file"]["name"]),
(string)$checksum,
$saasUser['user_id'] ?? null
);
foreach ($rows as $row) {
$amountCents = imports_normalize_amount($row['amount_raw']);
$timestamp = $row['date_raw'] !== '' ? strtotime($row['date_raw']) : false;
if ($amountCents === null || $timestamp === false) {
imports_store_row($pdo, $batchId, $row['row_number'], null, $row['raw_name'], $amountCents ?? 0, date('Y-m-d H:i:s', $timestamp ?: time()), 'invalid', $row['raw']);
continue;
}
$bookedAt = date('Y-m-d H:i:s', $timestamp);
$participant = imports_find_participant($pdo, $tenantId, $row['raw_name']);
if ($participant === null) {
imports_store_row($pdo, $batchId, $row['row_number'], null, $row['raw_name'], $amountCents, $bookedAt, 'unmatched', $row['raw']);
continue;
}
if (imports_is_duplicate($pdo, $tenantId, $participant['participant_id'], $amountCents, date('Y-m-d', $timestamp))) {
imports_store_row($pdo, $batchId, $row['row_number'], $participant['participant_id'], $row['raw_name'], $amountCents, $bookedAt, 'duplicate', $row['raw']);
continue;
}
imports_store_row($pdo, $batchId, $row['row_number'], $participant['participant_id'], $row['raw_name'], $amountCents, $bookedAt, 'matched', $row['raw']);
}
$pdo->commit();
$vorschauBatchId = $batchId;
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
$fehler = 'Die CSV-Datei konnte nicht verarbeitet werden.';
} finally {
@unlink($csvFile);
}
}
} elseif ($aktion === 'importieren') {
$batchId = (int)($_POST['batch_id'] ?? 0);
try {
$ergebnis = imports_commit_batch($pdo, $tenantId, $batchId);
app_audit_log($pdo, $tenantId, $saasUser['user_id'] ?? null, 'csv_import.committed', 'payment_import_batch', $batchId, ['imported' => $ergebnis['imported']]);
$meldung = $ergebnis['imported'] . ' Einzahlungen wurden importiert.';
} catch (Throwable $e) {
$fehler = $e->getMessage();
$vorschauBatchId = $batchId;
}
}
}
$vorschau = $vorschauBatchId !== null ? imports_fetch_batch($pdo, $tenantId, $vorschauBatchId) : null;
?>
<h2>CSV-Import</h2>
<?php if ($meldung !== null): ?>
<div class="hint-box success"><p><?php echo saas_html($meldung); ?></p></div>
<?php endif; ?>
<?php if ($fehler !== null): ?>
<div class="hint-box error"><p><?php echo saas_html($fehler); ?></p></div>
<?php endif; ?>
<?php if ($vorschau !== null): ?>
<h3>Vorschau: <?php echo saas_html($vorschau['batch']['original_filename']); ?></h3>
<p>Status: <?php echo saas_html($vorschau['batch']['status']); ?></p>
<table>
<tr>
<th>Zeile</th>
<th>Name (CSV)</th>
<th>Zugeordnetes Mitglied</th>
<th>Betrag</th>
<th>Datum</th>
<th>Status</th>
</tr>
<?php foreach ($vorschau['rows'] as $row): ?>
<tr>
<td><?php echo (int)$row['row_num']; ?></td>
<td><?php echo saas_html($row['raw_name']); ?></td>
<td><?php echo saas_html($row['display_name'] ?? '—'); ?></td>
<td><?php echo saas_html(number_format(((int)$row['amount_cents']) / 100, 2, ',', '.')); ?> €</td>
<td><?php echo saas_html($row['booked_at']); ?></td>
<td><?php echo saas_html(csv_status_label($row['status'])); ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php if ($vorschau['batch']['status'] === 'previewed'): ?>
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
<?php echo app_csrf_field(); ?>
<input type="hidden" name="aktion" value="importieren">
<input type="hidden" name="batch_id" value="<?php echo (int)$vorschau['batch']['id']; ?>">
<button type="submit">Import bestätigen</button>
</form>
<?php endif; ?>
<br>
<?php endif; ?>
<form action="csvupload.php" method="post" enctype="multipart/form-data">
<?php echo app_csrf_field(); ?>
<input type="hidden" name="aktion" value="hochladen">
<label for="csv_file">CSV-Datei auswählen:</label>
<input type="file" name="csv_file" accept=".csv" required>
<button type="submit">Datei hochladen</button>
</form>
</div>
</section>
<?php include "footer.php"; ?>