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

387 lines
14 KiB
PHP

<?php
require_once __DIR__ . "/app/bootstrap.php";
// Marketingdomain (z. B. kaffeeliste.de) zeigt die Landingpage statt des
// Dashboards; nur der Host aus APP_HOST bekommt die eigentliche App. Ohne
// gesetztes APP_HOST (lokale Entwicklung) verhaelt sich index.php wie
// bisher immer als Dashboard.
$appHost = app_primary_host();
$requestHost = strtolower(preg_replace('/:\d+$/', '', (string)($_SERVER['HTTP_HOST'] ?? '')));
if ($appHost !== null && $requestHost !== strtolower($appHost)) {
require __DIR__ . '/landing.php';
exit;
}
include "functions.php";
require_once __DIR__ . "/app/ledger.php";
app_require_csrf();
$pdo = app_db_pdo();
$saasUser = saas_current_user($pdo);
$tenantId = 0;
$hasAccess = false;
$emailNorm = '';
if ($saasUser !== null) {
$tenantId = (int)$saasUser['tenant_id'];
$emailNorm = strtolower(trim((string)$saasUser['email_norm']));
$hasAccess = true;
}
$participant = null;
if ($hasAccess) {
// Eingeloggte SaaS-Nutzer werden primaer ueber die feste user_id-
// Verknuepfung ihres Teilnehmers gefunden (bei Registrierung bzw.
// Zugangsvergabe gesetzt) - robuster als der E-Mail-Abgleich, der bei
// abweichender oder geleerter Teilnehmer-E-Mail ins Leere liefe. Der
// Legacy-/Dev-Pfad ohne SaaS-User faellt weiterhin auf die E-Mail zurueck.
$saasUserId = $saasUser !== null ? (int)$saasUser['user_id'] : 0;
if ($saasUserId > 0) {
$summaries = ledger_fetch_participant_summaries($pdo, $tenantId, [
'user_ids' => [$saasUserId],
]);
$participant = $summaries[0] ?? null;
}
if ($participant === null && $emailNorm !== '') {
$summaries = ledger_fetch_participant_summaries($pdo, $tenantId, [
'email_norms' => [$emailNorm],
]);
$participant = $summaries[0] ?? null;
}
}
$settings = $hasAccess ? saas_fetch_tenant_settings($pdo, $tenantId) : null;
$entryMessage = null;
$entryError = null;
// Ergebnis einer vorangegangenen Aktion (Post-Redirect-Get): nach dem Buchen
// bzw. Stornieren wird auf index.php zurueckgeleitet, damit ein Browser-Refresh
// die Aktion nicht ein zweites Mal ausloest.
if (isset($_SESSION['flash_dashboard'])) {
$flash = $_SESSION['flash_dashboard'];
unset($_SESSION['flash_dashboard']);
if (($flash['type'] ?? '') === 'success') {
$entryMessage = (string)$flash['text'];
} else {
$entryError = (string)$flash['text'];
}
}
function dashboard_money(int $cents): string
{
return saas_format_money_cents($cents) . ' €';
}
function dashboard_date(string $value): string
{
try {
return (new DateTimeImmutable($value))->format('d.m.Y');
} catch (Throwable $e) {
return $value;
}
}
function dashboard_current_name(?array $saasUser, ?array $participant): string
{
$name = trim((string)($saasUser['display_name'] ?? ''));
if ($name !== '') {
return $name;
}
return trim((string)($participant['display_name'] ?? ''));
}
function dashboard_paypal_action(string $template, string $amount): string
{
return trim($template) . $amount;
}
/**
* @return array{ok: bool, message?: string, error?: string}
*/
function dashboard_record_self_entry(PDO $pdo, array $participant, array $settings, mixed $marksValue, ?int $actorUserId): array
{
if ((int)$settings['self_entry_enabled'] !== 1) {
return ['ok' => false, 'error' => 'Die eigene Stricherfassung ist aktuell deaktiviert.'];
}
$marks = filter_var($marksValue, FILTER_VALIDATE_INT);
if ($marks !== 1 && $marks !== 2) {
return ['ok' => false, 'error' => 'Bitte einen oder zwei Striche auswählen.'];
}
$unitPriceCents = (int)$settings['mark_price_cents'];
if ($unitPriceCents <= 0) {
return ['ok' => false, 'error' => 'Der Preis pro Strich ist nicht gültig konfiguriert.'];
}
try {
ledger_record_consumption(
$pdo,
(int)$participant['tenant_id'],
(int)$participant['participant_id'],
$marks,
$unitPriceCents,
'self_entry',
$actorUserId
);
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
return ['ok' => false, 'error' => 'Die Stricheintragung konnte nicht gespeichert werden.'];
}
return ['ok' => true, 'message' => 'Stricheintragung wurde erfolgreich eingetragen.'];
}
function dashboard_render_payments(array $entries): void
{
echo "<h4>Letzte Einzahlungen</h4>";
echo "<table><tr><th style='width:120px'>Datum</th><th>Einzahlung</th></tr>";
if ($entries === []) {
echo "<tr><td colspan='2'>Keine Einzahlungen vorhanden.</td></tr>";
}
foreach ($entries as $entry) {
echo "<tr>";
echo "<td>" . saas_html(dashboard_date((string)$entry['booked_at'])) . "</td>";
echo "<td>" . saas_html(dashboard_money((int)$entry['amount_cents'])) . "</td>";
echo "</tr>";
}
echo "</table>";
}
function dashboard_render_consumption(array $entries): void
{
echo "<h4>Letzte Striche</h4>";
echo "<table><tr><th style='width:120px'>Datum</th><th>Striche</th><th>Kosten</th></tr>";
if ($entries === []) {
echo "<tr><td colspan='3'>Keine Striche vorhanden.</td></tr>";
}
foreach ($entries as $entry) {
echo "<tr>";
echo "<td>" . saas_html(dashboard_date((string)$entry['booked_at'])) . "</td>";
echo "<td>" . number_format((int)($entry['marks_count'] ?? 0), 0, ',', '.') . "</td>";
echo "<td>" . saas_html(dashboard_money(abs((int)$entry['amount_cents']))) . "</td>";
echo "</tr>";
}
echo "</table>";
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$flash = ['type' => 'error', 'text' => 'Die Aktion konnte nicht ausgeführt werden.'];
if ($hasAccess && $participant !== null && $settings !== null) {
$aktion = (string)($_POST['aktion'] ?? 'strich_eintragen');
if ($aktion === 'strich_stornieren') {
$result = ledger_void_own_self_entry($pdo, $tenantId, (int)$participant['participant_id']);
$flash = $result['ok']
? ['type' => 'success', 'text' => 'Dein zuletzt selbst eingetragener Strich wurde storniert.']
: ['type' => 'error', 'text' => $result['error'] ?? 'Der Strich konnte nicht storniert werden.'];
} else {
$result = dashboard_record_self_entry($pdo, $participant, $settings, $_POST["anzahlStriche"] ?? null, $saasUser['user_id'] ?? null);
$flash = $result['ok']
? ['type' => 'success', 'text' => $result['message'] ?? 'Stricheintragung wurde erfolgreich eingetragen.']
: ['type' => 'error', 'text' => $result['error'] ?? 'Die Stricheintragung konnte nicht gespeichert werden.'];
}
}
// Post-Redirect-Get: Ergebnis in die Session legen und per GET erneut
// anzeigen, damit ein Refresh die Buchung/Stornierung nicht wiederholt.
$_SESSION['flash_dashboard'] = $flash;
header('Location: index.php');
exit;
}
$paymentEntries = $participant !== null
? ledger_fetch_recent_entries($pdo, $tenantId, [
'participant_id' => $participant['participant_id'],
'type' => 'payment',
'limit' => 100,
])
: [];
$consumptionEntries = $participant !== null
? ledger_fetch_recent_entries($pdo, $tenantId, [
'participant_id' => $participant['participant_id'],
'type' => 'consumption',
'limit' => 100,
])
: [];
include "header.php";
include "headerline.php";
include "nav.php";
?>
<section id="banner">
<div class="content">
<?php
if ($hasAccess && $participant !== null && $settings !== null) {
$balanceCents = (int)$participant['balance_cents'];
$balance = dashboard_money($balanceCents);
$currentName = dashboard_current_name($saasUser, $participant);
$selfEntryEnabled = (int)$settings['self_entry_enabled'] === 1;
$paypalEnabled = (int)$settings['paypal_enabled'] === 1
&& trim((string)$settings['paypal_url_template']) !== '';
$cashEnabled = (int)$settings['cash_enabled'] === 1;
$bankTransferEnabled = (int)$settings['bank_transfer_enabled'] === 1
&& trim((string)$settings['bank_iban']) !== '';
$anyPaymentEnabled = $paypalEnabled || $cashEnabled || $bankTransferEnabled;
?>
<h2>Kaffeeliste</h2>
<?php if ($currentName !== ''): ?>
Hallo <?php echo saas_html($currentName); ?>!<br><br>
<?php endif; ?>
<?php if ($entryMessage !== null): ?>
<div class="hint-box success"><p><b><?php echo saas_html($entryMessage); ?></b><br>Du kannst den Eintrag weiter unten kontrollieren.</p></div><br>
<?php endif; ?>
<?php if ($entryError !== null): ?>
<div class="hint-box error"><p><b><?php echo saas_html($entryError); ?></b></p></div><br>
<?php endif; ?>
<?php
if ($balanceCents > 0) {
echo "<p><b>Aktueller Stand: " . saas_html($balance) . " (Guthaben)</b></p>";
} elseif ($balanceCents < 0) {
echo "<p><b>Aktueller Stand: " . saas_html($balance) . " (Schulden)</b></p>";
} else {
echo "<p><b>Aktueller Stand: " . saas_html($balance) . "</b></p>";
}
?>
<h2>Jahresübersicht</h2>
Ausgabe im aktuellen Jahr: <?php echo saas_html(dashboard_money((int)$participant['year_costs_cents'])); ?><br>
Gesamtstriche im aktuellen Jahr: <?php echo number_format((int)$participant['year_marks'], 0, ',', '.'); ?><br>
<p>Gesamteinzahlung im aktuellen Jahr: <?php echo saas_html(dashboard_money((int)$participant['year_payments_cents'])); ?></p>
<?php
$aktuellesJahr = (int)date('Y');
$monatsdaten = ledger_fetch_monthly_consumption($pdo, $tenantId, (int)$participant['participant_id'], $aktuellesJahr);
$monatsnamen = [1 => 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
$aktuellerMonat = (int)date('n');
if ((int)$participant['year_marks'] > 0):
?>
<h2>Monatsübersicht <?php echo $aktuellesJahr; ?></h2>
<table>
<tr><th style="width:160px">Monat</th><th>Striche</th><th>Verbrauch</th></tr>
<?php
for ($m = 1; $m <= $aktuellerMonat; $m++):
$md = $monatsdaten[$m];
?>
<tr>
<td><?php echo saas_html($monatsnamen[$m]); ?></td>
<td><?php echo number_format($md['marks'], 0, ',', '.'); ?></td>
<td><?php echo saas_html(dashboard_money($md['cost_cents'])); ?></td>
</tr>
<?php endfor; ?>
</table>
<?php endif; ?>
<?php if ($selfEntryEnabled): ?>
<h2>Eintrag in die Strichliste</h2>
<b>Hier kannst du einen oder zwei Striche für dich in der Kaffeeliste eintragen. <br>Dafür benötigst du keinen Eintrag auf der Liste durchführen.</b><br>
Aktueller Preis pro Strich: <?php echo saas_html(dashboard_money((int)$settings['mark_price_cents'])); ?><br><br>
<ul class="actions">
<li>
<form method="post" action="index.php">
<button type="submit">Einen Strich eintragen</button>
<input type="hidden" name="aktion" value="strich_eintragen">
<input type="hidden" name="anzahlStriche" value="1">
<?php echo app_csrf_field(); ?>
</form>
</li>
<li>
<form method="post" action="index.php">
<button type="submit">Zwei Striche eintragen</button>
<input type="hidden" name="aktion" value="strich_eintragen">
<input type="hidden" name="anzahlStriche" value="2">
<?php echo app_csrf_field(); ?>
</form>
</li>
<li>
<form method="post" action="index.php" onsubmit="return confirm('Deinen zuletzt selbst eingetragenen Strich stornieren?');">
<button type="submit" class="alt">Letzten Strich stornieren</button>
<input type="hidden" name="aktion" value="strich_stornieren">
<?php echo app_csrf_field(); ?>
</form>
</li>
</ul>
<small>Falsch getippt? Mit „Letzten Strich stornieren" nimmst du deinen jüngsten selbst eingetragenen Strich wieder zurück. Vom Kassenwart eingetragene Striche bleiben davon unberührt.</small>
<?php endif; ?>
<?php if ($anyPaymentEnabled): ?>
<h2>Bezahlen</h2>
<?php if ($balanceCents < 0): ?>
<p>Dein offener Betrag: <b><?php echo saas_html(dashboard_money(abs($balanceCents))); ?></b></p>
<?php endif; ?>
<?php endif; ?>
<?php if ($cashEnabled): ?>
<h4>Barzahlung</h4>
<?php if (trim((string)$settings['cash_contact']) !== ''): ?>
<p>Bitte bar bei <b><?php echo saas_html($settings['cash_contact']); ?></b> bezahlen.</p>
<?php else: ?>
<p>Barzahlung ist möglich.</p>
<?php endif; ?>
<?php endif; ?>
<?php if ($bankTransferEnabled): ?>
<h4>Überweisung</h4>
<p>
<?php if (trim((string)$settings['bank_account_holder']) !== ''): ?>
Kontoinhaber: <b><?php echo saas_html($settings['bank_account_holder']); ?></b><br>
<?php endif; ?>
IBAN: <b><?php echo saas_html(trim(chunk_split((string)$settings['bank_iban'], 4, ' '))); ?></b>
<?php if ($balanceCents < 0): ?>
<br>Bitte überweise deinen offenen Betrag von <b><?php echo saas_html(dashboard_money(abs($balanceCents))); ?></b>.
<?php endif; ?>
</p>
<?php endif; ?>
<?php if ($paypalEnabled): ?>
<h4>PayPal</h4>
<b>Bezahle immer über die Freunde-Funktion von PayPal. Ansonsten stellen wir 20% des Betrags als Gebühr in Rechnung.</b><br>
<br>
<?php
$paypalTemplate = trim((string)$settings['paypal_url_template']);
if ($balanceCents < 0) {
$debtCents = abs($balanceCents);
$debtAmount = dashboard_money($debtCents);
echo '<form action="' . saas_html(dashboard_paypal_action($paypalTemplate, saas_format_money_cents($debtCents))) . '" target="_blank"><button type="submit">' . saas_html($debtAmount) . ' bezahlen</button></form>';
}
?>
<ul class="actions">
<li><form action="<?php echo saas_html(dashboard_paypal_action($paypalTemplate, '5')); ?>" target="_blank"><button type="submit">5,00 € einzahlen</button></form></li>
<li><form action="<?php echo saas_html(dashboard_paypal_action($paypalTemplate, '10')); ?>" target="_blank"><button type="submit">10,00 € einzahlen</button></form></li>
<li><form action="<?php echo saas_html(dashboard_paypal_action($paypalTemplate, '15')); ?>" target="_blank"><button type="submit">15,00 € einzahlen</button></form></li>
</ul>
<?php endif; ?>
<br><br>
<?php
dashboard_render_payments($paymentEntries);
echo "<br>";
dashboard_render_consumption($consumptionEntries);
?>
<br><br>
<form action="namenanpassen.php" method="get">
<button type="submit">Namensanpassung</button>
</form>
<?php
} elseif ($hasAccess) {
echo "<h2>Kaffeeliste</h2>";
echo "<p>Für dein Konto wurde im aktuellen Mandanten kein Teilnehmer gefunden.</p>";
} else {
echo "<h2>Sie haben keinen Zugang zu dieser Webseite</h2>";
}
?>
</div>
</section>
<?php include "footer.php"; ?>