Commit Graph
119 Commits
Author SHA1 Message Date
clemens c86803bbd6 Matomo-Tracking und Aufrufzaehlung absichern 2026-08-27 12:20:56 +02:00
clemens 528af07890 Matomo-Tracking mit Einwilligung einbauen 2026-08-27 09:20:46 +02:00
clemensandClaude Opus 5 a2dcc4df65 Zwei-Faktor-Anmeldung per TOTP
app/totp.php implementiert RFC 6238 selbst statt per Bibliothek: der
Algorithmus ist ein HMAC plus eine Truncation, und ein zweiter Faktor ist
die letzte Stelle fuer ungepruefte Abhaengigkeiten. Der QR-Code entsteht
aus dem ohnehin vorhandenen TCPDF, damit kein externer Dienst das
Geheimnis sieht.

Beim Login wird die Anmeldung bei aktivem zweitem Faktor nicht
abgeschlossen; der Zwischenzustand gewaehrt keinerlei Zugriff und ist
byte-identisch zu einem unangemeldeten Aufruf. users.totp_last_step
verhindert die Wiederverwendung eines abgefangenen Codes innerhalb seines
Gueltigkeitsfensters. Abschalten verlangt Passwort und Code.

APP_REQUIRE_2FA_FOR_ADMINS macht den Faktor fuer Platform-Admins
verbindlich, per Weiterleitung auf die Einrichtung statt als harte Sperre
- sonst koennte der Schalter den einzigen Admin aussperren. Standard aus.

check-konto-und-mandantenwechsel erwartete beim Login noch das entfernte
Kundenkuerzel-Feld und damit einen direkten Sprung aufs Dashboard; der
Check bildet jetzt den tatsaechlichen Weg ueber die Mandantenauswahl ab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 23:51:45 +02:00
clemensandClaude Opus 5 86334c2752 Offene Registrierung gegen Bot-Massenanlage absichern
Auf der Testumgebung liefen seit Juli 2026 taeglich 10-20 automatisierte
Registrierungen, die zufaellige Namen mit fremden echten E-Mail-Adressen
kombinierten - der Server verschickte Vertrags- und Verifikationsmail an
Unbeteiligte. Das IP-Rate-Limit griff nicht, weil jede Anfrage ueber eine
eigene Rechenzentrums-IP kam.

app/spam-guard.php ergaenzt daher Honigtopf, Zeitfalle, eine globale
Notbremse ueber alle IPs hinweg und eine MX/A-Pruefung der Mail-Domain.
Die ersten drei antworten mit derselben generischen Meldung wie das
Rate-Limit, damit die Antwort nicht verraet, welche Huerde angeschlagen
hat; Treffer landen im Error-Log. Bewusst ohne Captcha, um keinen
Drittanbieter in den Registrierungspfad zu holen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 23:09:38 +02:00
clemensandClaude Opus 5 3346b51b21 Standangabe auch auf der Bestellseite entfernen
Analog zur Registrierung: AGB und Widerrufsbelehrung nennen die
Dokumentversion nicht mehr im Checkbox-Label. Aufgezeichnet wird sie
weiterhin in abo-upgrade.php ueber app_record_legal_acceptance().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 22:28:16 +02:00
clemensandClaude Opus 5 d1111983b0 Standangabe bei den Rechtstexten der Registrierung entfernen
Die Checkbox-Labels fuer AGB, Datenschutzerklaerung und AVV nannten die
Dokumentversion in Klammern. Die Angabe entfaellt in der Anzeige; die
tatsaechlich akzeptierte Version wird davon unberuehrt weiterhin ueber
app_record_legal_acceptance() gespeichert und in der Vertragsmail
genannt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 21:56:23 +02:00
clemensandClaude Opus 5 27292470f6 Abstand zwischen Formular und Button-Zeile in den Auth-Panels
ul.actions bringt keinen oberen Abstand mit, deshalb klebte die
Button-Zeile direkt am letzten Eingabefeld - im Login besonders
sichtbar, seit das Kundenkuerzel-Feld entfallen ist. Die Regel ist auf
.public-auth beschraenkt, damit die CTA-Panels der Landingpage ihr
Layout behalten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 21:09:17 +02:00
clemensandClaude Opus 5 47dff5a1ca Kundenkuerzel aus Login und Passwort-vergessen entfernen
Das Feld war optional und musste vom Kunden auswendig gewusst werden.
Ohne Einschraenkung liefert saas_authenticate() bei mehreren Mandanten
needs_tenant_selection, und mandant-auswahl.php laesst per Klarnamen
waehlen - der kundenfreundlichere Weg. Beim Passwort-Reset haengt das
Passwort ohnehin am Benutzerkonto, nicht am Mandanten.

Die optionalen Parameter von saas_authenticate() und
saas_request_password_reset() bleiben erhalten; die check-*-Skripte
pruefen darueber weiterhin die mandantenspezifische Aufloesung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 20:45:44 +02:00
clemensandClaude Opus 5 93d6bd9127 Pushover-Benachrichtigung bei neuer Registrierung
Neuer Helfer app/pushover.php meldet Selbstregistrierungen an den
Betreiber. Der Aufruf sitzt in register.php statt in
saas_register_tenant_owner(), damit die check-*-Skripte keine Pushes
ausloesen, und wertet das Ergebnis nicht aus: ein fehlgeschlagener Push
darf die Registrierung nicht abbrechen. Ohne PUSHOVER_TOKEN/PUSHOVER_USER
ist die Funktion still deaktiviert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 20:39:22 +02:00
clemens 213dd10dbd Produktiv- und Testtabellen per Prefix trennen 2026-08-22 15:29:52 +02:00
clemens a31a235422 Rechtstexte und B2C-Vertragsabläufe absichern 2026-08-22 14:31:56 +02:00
clemens d320a4fd7a Windows-1252-PayPal-Mails verarbeiten 2026-08-22 00:17:17 +02:00
clemens a0d8c62082 Absenderpruefung bei PayPal-Mails entfernen 2026-08-22 00:03:48 +02:00
clemensandClaude Opus 5 43d0fc5578 Absenderpruefung erweitern und Postfach auflisten koennen
Die Pruefung suchte woertlich nach "@paypal.de" oder "@paypal.com". Mails
aus einer Versand-Subdomain (e.paypal.de) oder einer Landesvariante
(paypal.at, paypal.co.uk) fielen damit als not_from_paypal durch. Geprueft
wird jetzt die Domain der Absenderadresse an ihrem Ende - "paypal.de.
beispiel.com" ist damit weiterhin kein PayPal, waere bei einer Textsuche
aber durchgerutscht.

check-imap-support.php listet zusaetzlich die letzten 15 Mails mit
Absender, Empfaengerzeilen und Betreff auf und sagt je Mail, ob der
Absender als PayPal gilt. Damit laesst sich ein not_from_paypal in einem
Lauf klaeren statt zu raten. Rein lesend: OP_READONLY, kein \Seen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 22:35:51 +02:00
clemensandClaude Opus 5 d7de0d92cf PayPal-Zahlungen auch ueber die Mailadresse zuordnen
Bisher verglich die Zuordnung ausschliesslich Namen: der Parser las die
Adresse des Zahlers gar nicht aus, und die Suche kannte nur paypal_name und
display_name. Eine Zahlung von genau der Adresse, die beim Mitglied
hinterlegt ist, blieb deshalb liegen.

Die Adresse wird jetzt ausgelesen (neue Spalte paypal_payments.payer_email),
in der Warteschlange angezeigt und zuerst geprueft - sie ist im Mandanten
eindeutig und aendert sich nicht, wenn jemand bei PayPal anders heisst.

Der heikle Teil ist das Aussortieren: In einer weitergeleiteten Mail stehen
mehrere Adressen. Die falsche zu nehmen wuerde fremdes Geld dem Mitglied
hinter dieser Adresse gutschreiben - typischerweise dem Kassenwart. Deshalb
fallen paypal.*-Adressen und die eigene Eingangsadresse raus (geprueft
gegen die tatsaechliche Empfaengeradresse, Plus-Adressierung ignoriert),
und es zaehlt nur eine Adresse in unmittelbarer Naehe des Zahlernamens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 22:17:48 +02:00
clemensandClaude Opus 5 25bdb6f34b PayPal-Name aus der manuellen Zuordnung lernen
Die automatische Zuordnung verlangt exakte Uebereinstimmung des
Zahlernamens mit Anzeigename oder PayPal-Name des Mitglieds. Heisst jemand
bei PayPal anders, landet jede Zahlung in der Warteschlange - auch die
zehnte, obwohl der Fall laengst einmal von Hand geklaert wurde.

Nach einer manuellen Zuordnung wird der Zahlername deshalb beim Mitglied
hinterlegt. Zurueckhaltend: ein gepflegter PayPal-Name wird nie
ueberschrieben, ein Name gleich dem Anzeigenamen bringt nichts, und passt
der Name auch auf ein anderes Mitglied, wird nichts gelernt - das wuerde
die Zuordnung mehrdeutig machen und damit blockieren.

Was gelernt wurde, steht in der Erfolgsmeldung und im Audit-Log; eine
stillschweigende Aenderung am Mitglied waere sonst schwer nachvollziehbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 22:03:12 +02:00
clemensandClaude Opus 5 f4b57abd93 imap-Erweiterung in der lokalen PHP-Umgebung ergaenzt
Ohne Root laesst sich das .deb trotzdem auspacken und einbinden. Damit
laeuft der PayPal-Mailabruf jetzt auch lokal testbar - bisher fiel er hier
sofort mit "Die PHP-IMAP-Erweiterung ist nicht installiert" aus.

Die Schritte stehen in docs/dev-mysql.md, weil .local/ von Git ignoriert
wird und die Umgebung sonst nicht reproduzierbar waere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 21:21:23 +02:00
clemensandClaude Opus 5 2fd606a3aa Diagnose fuer den PayPal-Postfachzugang
Vor dem ersten Abruf soll klar sein, ob der Host das Postfach ueberhaupt
erreicht - und auf welchem Weg. Das Skript meldet PHP-Version, ob die
imap-Erweiterung geladen ist, ob die PAYPAL_*-Einstellungen gesetzt sind,
und oeffnet das Postfach testweise ueber eine reine TLS-Verbindung.

Damit ist auch ohne die Erweiterung belegbar, dass ein Abruf moeglich
waere - seit PHP 8.4 ist imap kein Bestandteil von PHP mehr und laesst
sich auf einem Webhosting nicht einfach nachruesten.

Bewusst nur lesend: EXAMINE statt SELECT, kein Flag wird gesetzt. Das
Passwort wird nie ausgegeben, nur seine Laenge als Tippfehler-Probe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 21:13:27 +02:00
clemensandClaude Opus 5 9f9e4c7cd0 PayPal-Mailabruf: Parken statt Buchen, echter Probelauf, Regressionstest
Die Mail-Verarbeitung ignorierte bisher die PayPal-Schalter: eingehende
Zahlungen wurden auch dann automatisch gutgeschrieben, wenn der Betreiber
die Funktion gesperrt oder der Mandant PayPal abgeschaltet hatte. Jetzt
wird die Zahlung in dem Fall geparkt - gespeichert, aber ohne Buchung.
Wegwerfen liesse eine echte Zahlung unbemerkt verschwinden, buchen
widersprache der Abschaltung; die Zuordnungsseite bleibt fuer offene
Zahlungen ja erreichbar.

--dry-run war bisher irrefuehrend: es liess die Verarbeitung samt Buchung
laufen und uebersprang nur das Setzen des Gelesen-Flags - ausgerechnet beim
ersten Testlauf haette es also echtes Geld verbucht. Der Probelauf nutzt
jetzt paypal_preview(), das nichts schreibt und meldet, was passieren
wuerde (would_book/would_queue/would_park/duplicate).

scripts/check-paypal-inbox-flow.php deckt die Kette ohne IMAP ab:
Absenderpruefung, Token, Parser, Zuordnung, Netto-Buchung, Dedup, beide
Park-Faelle und die Schreibfreiheit der Vorschau.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 20:43:58 +02:00
clemensandClaude Opus 5 7df9c1fb4e Marke im PDF um Faktor 1,5 vergroessert
Die erste Fassung war auf dem Ausdruck zu klein. Alle Masse haengen jetzt
an PLAN_WATERMARK_SCALE, damit Schrift und Tasse gemeinsam wachsen und die
Proportionen stimmen; das reservierte Band am Seitenfuss waechst mit
(PLAN_WATERMARK_BAND_MM), damit die Tabelle weiterhin Abstand haelt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:01:04 +02:00
clemensandClaude Opus 5 9b309bb942 Hinweis auf die Kaffeeliste auch im PDF-Ausdruck
Die Marke stand bisher nur in der Web-Ansicht; auf dem Ausdruck, den die
Mitglieder taeglich vor sich haben, fehlte sie. Sie erscheint jetzt unten
rechts im Seitenfuss - nach derselben Regel wie im Web: im kostenlosen
Tarif fest, sonst nach Mandant-Einstellung.

Die Tasse wird mit TCPDFs Zeichenbefehlen gemalt statt als SVG oder Bild
eingebettet: ImageSVG braucht die XML-Extension, ein Rasterbild GD - beides
ist auf einem Webspace nicht garantiert.

Der Schriftzug sitzt in einer Cell() statt in Text(): dessen y-Bezug haengt
an Schriftmetriken und ergab in der Probe einen um Millimeter versetzten
Schriftzug. Steht die Marke, reserviert export_page_row_budget_mm() 6 mm am
Seitenfuss, damit die Tabelle nicht hineinlaeuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 00:46:52 +02:00
clemens 7a52099a59 Anpassung des impressums 2026-08-21 00:31:34 +02:00
clemensandClaude Opus 5 b244451c42 Schalter fuer den Hinweis auf die Kaffeeliste
Der Hinweis unten rechts laesst sich ab einem bezahlten Tarif in den
Mandant-Einstellungen abschalten (neue Spalte tenant_settings.
watermark_enabled, Standard an). Im kostenlosen Tarif bleibt er fest
eingeschaltet: das Kaestchen ist angehakt und disabled, daneben steht der
Grund. Zusaetzlich erzwingt saas_update_tenant_settings() den Wert dort noch
einmal, damit ein nachgebauter POST ihn nicht abschalten kann.

Der Titel des Wasserzeichens spricht nicht mehr vom kostenlosen Tarif -
zahlende Mandanten koennen es jetzt freiwillig zeigen.

Der Smoke-Test prueft die Einstellungsseite jetzt auch angemeldet. Sein
Muster fuer PHP-Meldungen verlangt dafuer einen Doppelpunkt, sonst haette
das Feld "negative_warning" als Warnung gezaehlt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 00:27:44 +02:00
clemensandClaude Opus 5 1d53b66e3f Wasserzeichen fuer den kostenlosen Tarif
Mandanten im Tarif "free" (bis 10 Teilnehmer) zeigen unten rechts eine
kleine Marke mit Logo und Adresse der Kaffeeliste; zahlende Mandanten
behalten ihre Oberflaeche ohne fremdes Logo.

Die Sichtbarkeit haengt allein an tenant_billing.plan_code und wird als
reine Leseabfrage geprueft - billing_fetch_or_init() waere ein Schreibzugriff
im Seitenaufbau. Laesst sich der Tarif nicht lesen, bleibt die Marke aus.
Das Logo ist ein Inline-SVG, das Linkziel kommt aus APP_MARKETING_URL bzw.
aus APP_HOST ohne App-Subdomain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 00:14:17 +02:00
clemensandClaude Opus 5 6cd7fcc079 Offene PayPal-Zahlungen bleiben nach dem Abschalten erreichbar
Wird PayPal in den Mandant-Einstellungen abgeschaltet, waehrend noch
Zahlungen unzugeordnet in der Warteschlange liegen, kaeme ohne diese
Ausnahme niemand mehr an sie heran - weder ueber das Menue noch ueber die
URL.

paypal_inbox_accessible() haelt Menuepunkt und Seite deshalb offen, solange
paypal_count_unmatched() etwas findet; die Seite weist per Hinweis darauf
hin, dass sie nach dem Abarbeiten verschwindet. Eine Sperre durch den
Betreiber sticht die Ausnahme.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 23:42:52 +02:00
clemensandClaude Opus 5 3f6866e075 Menue folgt auch den Schaltern des Mandanten
Bisher blendete das Menue nur aus, was der Betreiber gesperrt hatte. Hat
der Mandant selbst eine Funktion abgeschaltet - etwa "PayPal anbieten" in
den Mandant-Einstellungen - blieb der Menuepunkt stehen und fuehrte auf
eine Seite ohne Zweck.

app_feature_available() prueft nun beide Ebenen, app_feature_tenant_switch()
haelt die Zuordnung Funktion -> Mandanten-Schalter. Navigation und Anleitung
nutzen die neue Pruefung; paypal-zuordnung.php sperrt sich beim direkten
Aufruf ebenfalls und verweist dabei auf die Mandant-Einstellungen statt auf
den Betreiber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 23:16:30 +02:00
clemensandClaude Opus 5 d24ee9bf91 Betreiber-Zentralstelle, Mandanten-Design und schlankerer Einstieg
Funktions-Schalter je Mandant (app/features.php, tenant_features): der
Betreiber schaltet FAQ, Selbsteintrag, PDF, PayPal-Eingang, CSV-Import,
Mailversand, Jahresabschluss, Datenexport und eigenes Design pro Mandant
frei. Gesperrte Funktionen verschwinden aus Menue und Schaltflaechen, ihre
Seiten weisen Aufrufe und POSTs ab. Das Back-Office ist jetzt fuer
Platform-Admins im Menue verlinkt statt nur per URL erreichbar.

Eigenes Design je Mandant: Akzentfarbe und Logo in den Mandant-
Einstellungen, eingebettet ueber app/branding.php; das Logo liegt
geschuetzt in var/tenant_logos und wird nur ueber
tenant-logo-anzeigen.php an den eigenen Mandanten ausgeliefert.

Weniger Startinformationen: Startpaket neuer Mandanten auf zwei
Beispielfragen gekuerzt, Anleitung von ~1400 auf ~750 Woerter gestrafft
und um Abschnitte zu gesperrten Funktionen bereinigt.

Vorder-/Rueckseite erst bei mehr als 50 Personen (vorher schon ab 50).
Die Schwelle liegt jetzt gemeinsam in app/ledger.php und gilt auch fuer
die Vorder-/Rueckseiten-Auswahl beim Erfassen, wo sie bisher unabhaengig
von der Teamgroesse angeboten wurde.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:52:55 +02:00
clemens a93e067e04 Verbessere Mitgliederfilter und Zahlungszuordnung 2026-08-13 15:40:43 +02:00
clemens 0f263c3a19 Haerte Verwaltungsflows und Zahlungsabgleich 2026-08-07 12:02:41 +02:00
clemensandClaude Opus 5 39f662d541 Kontofunktionen vor dem Go-Live und Landingpage-Kuerzung
Vier Luecken geschlossen, die im Alltag sofort aufgefallen waeren:

- Passwort aendern war im eingeloggten Zustand gar nicht moeglich; es gab
  nur den Reset per Mail-Link. saas_change_password() prueft das aktuelle
  Passwort, verlangt ein tatsaechlich anderes und erneuert danach die
  Session-ID. Formular in konto.php.
- mandant-auswahl.php war nur direkt nach dem Login erreichbar. Wer bei
  mehreren Mandanten Mitglied ist, musste sich zum Wechseln abmelden. Die
  Seite bedient jetzt beide Wege, die Mandantenpruefung bleibt unveraendert
  ueber saas_identity_for_user_tenant(). Menuepunkt ab zwei Mitgliedschaften.
- email_verified_at wurde nirgends geprueft, nur angezeigt - bei offener
  Selbstregistrierung konnte sich jemand mit fremder Adresse anmelden und
  alles nutzen. Erzwungen wird jetzt gezielt dort, wo eine Aktion nach
  aussen wirkt: Einladung, Info-Mail, Jahresabschluss. Der Login selbst
  bleibt bewusst frei, sonst waeren alle migrierten Bestandsnutzer mit
  NULL-Verifikation ausgesperrt. Zusaetzlich setzt der Passwort-Reset die
  Verifikation mit, weil der Mail-Link den Postfachzugriff nachweist -
  sonst blieben eingeladene Mitglieder dauerhaft unbestaetigt.
- Login landete auf konto.php statt auf dem Dashboard.

Landingpage: die gruene Vertrauenszeile auf "DSGVO-konform" gekuerzt, die
Eintraege zu Paragraf 19 UStG und "Bestehende Ablaeufe bleiben" entfernt.

Geprueft: neues scripts/check-konto-und-mandantenwechsel.php mit 18
Assertions gruen, Passwortformular zusaetzlich manuell inkl. CSRF (419).
Bestehende Suiten unveraendert gruen: HTTP-Smoke 34 Seiten, Rollenmatrix
55, Mandanten-Isolation 12, M3-Auth 9, M3-Settings 15.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:03:50 +02:00
clemensandClaude Opus 5 52b1726698 Deployment-Vorbereitung: Ausschlussliste, .htaccess-Haertung, Doku
Vor dem ersten FTP-Deploy nach /testumgebung.kaffeeliste.de/httpdocs/
fehlte jede Absicherung des Upload-Umfangs: sync_config.jsonc hatte eine
leere excludePath-Liste, es waeren also .git (komplett herunterladbar),
.env.local (Dev-DB-Zugangsdaten) und sync_config.jsonc selbst (enthaelt
das FTP-Passwort im Klartext) mit ausgeliefert worden. Von diesen dreien
war keines von den bestehenden .htaccess-Regeln erfasst.

- sync_config.jsonc: excludePath gefuellt; scripts/ und database/ bleiben
  bewusst im Deploy, weil die Plesk-Scheduled-Tasks sie vom Webspace aus
  ausfuehren.
- .htaccess: sync_config.jsonc und Dotfile-Ordner gesperrt, Vendor-
  Verzeichnisse (TCPDF/PHPMailer/DataTables) fuer direkte URL-Aufrufe
  gesperrt, HTTPS-Redirect ergaenzt (ohne ihn bekam ein http-Besucher
  eine Session ohne secure-Flag).
- PHPMailer/ entfernt: enthielt nur noch LICENSE, composer.json und ein
  per URL erreichbares get_oauth_token.php, kein Quellcode. Der Versand
  laeuft seit M6 ueber saas_send_mail().
- docs/deployment.md: Umgebungen, Deploy-Ablauf, Migrationen und
  Cron-Jobs ueber Plesk Scheduled Tasks (kein SSH verfuegbar),
  Mail-/DNS-, Stripe- und PayPal-Voraussetzungen, Go-Live-Checkliste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 15:45:01 +02:00
clemens 3f564c361f sync config Ausnahme 2026-08-06 15:35:10 +02:00
clemensandClaude Opus 4.8 fb4dcf96fd M9-Plan: SAML/ADFS als Alternative dokumentiert
Ergaenzt den Abschnitt, was eine direkte SAML-Anbindung ans ADFS zusaetzlich
kosten wuerde - als Entscheidungsgrundlage neben dem beschlossenen OIDC-Weg,
nicht als beschlossener Weg.

Kernpunkte: SAML braucht ext-dom, ext-simplexml und ext-mbstring sowie
Composer; in der aktuellen Dev-Umgebung fehlt jede XML-Faehigkeit (nicht
einmal DOMDocument existiert), SAML liesse sich dort weder bauen noch testen.
Dazu eigenes SP-Zertifikat samt Erneuerung, eine Tabelle gegen
Wiedereinspielung, eine umfangreiche Pruefliste fuer eingehende Assertions und
der automatische Zertifikatswechsel von ADFS als haeufigste Stoerungsursache.

Empfehlung bleibt OIDC ueber Entra; SAML waere Stufe 3 nach Magic-Link und
OIDC, mit der Erweiterungspruefung auf dem Zielhost als erster Aufgabe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:44:25 +02:00
clemensandClaude Opus 4.8 c6a03c1a38 Tarif-Tabelle: hervorgehobene Zeile war keine Tabellenzeile mehr
Die aktuell gebuchte Tarifzeile trug class="hint-box success" auf dem <tr>.
.hint-box macht aus dem Element einen Flex-Container - auf einer Tabellenzeile
loest das die Spaltenaufteilung auf. Mit dem Grundstil aus main.css lagen die
Zellen noch nebeneinander und das fiel kaum auf; durch das in d18c55e
ergaenzte flex-direction: column standen sie dann untereinander.

Behoben an der Ursache statt am CSS: Die Zeile bekommt eine eigene Klasse
.tarif-aktuell und bleibt eine normale Tabellenzeile, die Hervorhebung laeuft
ueber Hintergrund und einen Akzentstreifen an der ersten Zelle. Zusaetzlich
ist die flex-direction-Regel jetzt auf div.hint-box eingegrenzt, damit die
Klasse auf einem Tabellenelement nicht erneut die Spalten zerstoeren kann.

Geprueft: http-smoke 34/0, role-matrix 55/0, settings-flow 15/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:48:17 +02:00
clemensandClaude Opus 4.8 143d7efc97 Planung M9: Anmeldung per Magic-Link und SSO ueber Entra ID
Noch keine Umsetzung - nur Datenmodell, Ablaeufe und Reihenfolge zum
Abschaetzen. Zwei unabhaengig lieferbare Stufen: erst Magic-Link (klein und
stellt danach den Notzugang bereit), dann OIDC gegen Entra ID.

Festgehaltene Entscheidungen: OIDC statt SAML, ADFS indirekt ueber Entra,
kein automatisches Anlegen von Konten beim SSO-Login, SSO-Pflicht pro Mandant
mit Magic-Link als Notzugang fuer owner/admin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 16:41:34 +02:00
clemensandClaude Opus 4.8 d18c55eef5 Formular-Abstaende korrigiert und Anleitungsseite ergaenzt
Abstaende:
Im Template hat das Label 1em Abstand nach unten, das Eingabefeld aber gar
keinen, und die Formular-Grids nutzen keinen vertikalen Gutter (gtr-uniform).
Dadurch war der Abstand vom Feld zum naechsten Label 0, waehrend Label zu
eigenem Feld 1em betrug - das Label wirkte, als gehoere es zum Feld darueber.
Jetzt sitzt das Label eng an seinem eigenen Feld (0,5em) und die Felder haben
1,5em Abstand nach unten.

Ausserdem stylt main.css die Typen number, date, datetime-local und file gar
nicht; diese Felder erschienen als kleine native Kaestchen zwischen den sonst
einheitlichen Feldern (z. B. "Listenfenster in Tagen" und "Zeilenhoehe im
Ausdruck"). Sie bekommen jetzt dasselbe Aussehen wie die Textfelder.

Die Anpassungen liegen in einer eigenen assets/css/app.css, damit main.css als
Template-Datei unveraendert bleibt. Nebenbei: .hint-box.error war nirgends
definiert, obwohl es die haeufigste Variante ist - Fehlermeldungen erschienen
ungefaerbt wie neutrale Hinweise.

In faq.php, hinweise.php, namenanpassen.php und stricheintragen.php standen
<br>/<br><br> als Abstandsersatz hinter Labels und Feldern; mit den neuen
Abstaenden waere daraus doppelter Leerraum geworden. Entfernt, das Layout
kommt jetzt aus dem CSS.

Anleitung:
Neue Seite anleitung.php mit den wichtigsten Funktionen, gegliedert nach
Zielgruppe. Die Abschnitte fuer Kassenwart bzw. Administration werden nur
eingeblendet, wenn die Rolle sie auch nutzen kann - sonst stuenden dort
Hinweise auf Menuepunkte, die es fuer den Nutzer nicht gibt. Verlinkt im Menue
unter "Kaffeeliste" und im Smoke-Test abgedeckt.

Geprueft: alle 12 Formularseiten rendern fehlerfrei, http-smoke 34/0,
role-matrix 55/0, tenant-isolation 12/0, settings-flow 15/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:51:14 +02:00
clemensandClaude Opus 4.8 064c872c30 Zeitzonen-Angleich PHP/MySQL + veralteten Settings-Test aktualisiert
Zwei vorbestehende, bislang rote Pruefungen behoben.

1) PHP lief in UTC, MySQL in der System-Zeitzone (hier CEST, +2h). Ueberall,
   wo ein in PHP berechneter Zeitstempel gegen MySQL NOW() verglichen wird,
   liefen die Uhren dadurch gegeneinander:
   - Hinweise mit kurzer Restlaufzeit galten sofort als abgelaufen
     (notices: valid_from = NOW() aus MySQL, valid_until aus PHP date()).
   - Auth-Token (Passwort-Reset, E-Mail-Verifikation) liefen bis zu 2h zu
     frueh ab (expires_at aus PHP date(), Pruefung gegen NOW()).
   bootstrap.php pinnt die PHP-Zeitzone jetzt deterministisch aus
   APP_TIMEZONE (Standard Europe/Berlin), app_db_pdo() setzt die DB-Session
   per numerischem Offset auf dieselbe Zeit. Damit stimmen beide Uhren
   ueberein. Behebt check-m8-tenant-isolation (11/1 -> 12/0).

2) check-m3-settings-flow stammte aus M3 und lieferte nicht die spaeter
   hinzugekommenen Pflichtfelder (pdf_row_height_px,
   payment_reminder_interval_days). Dadurch schlug bereits das Update fehl und
   alle Folge-Assertions kippten. Der Test sendet jetzt den vollstaendigen
   Feldsatz wie das Einstellungsformular und prueft die beiden Felder mit.
   Die Update-Funktion selbst war korrekt (7/6 -> 15/0).

Voller Regressionslauf gruen: http-smoke 33/0, role-matrix 55/0,
tenant-isolation 12/0, m3-auth 9/0, password-email 14/0, settings 15/0,
tenant-resolution 12/0, billing 10/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 08:40:55 +02:00
clemensandClaude Opus 4.8 4acd4a2d40 Legacy-Abbau Schritt 3+4: kl_*-Tabellen und legacy_*-Spalten entfernt
Abschluss des Legacy-Abbaus. Die Migration in participants/ledger_entries war
laut Dev-DB vollstaendig (keine legacy_mitarbeiter_id, kein legacy_table), die
kl_*-Tabellen enthielten nur noch Golden-Master-Testfixtures. Da kein echter
Altbestand mehr importiert werden muss (Prod entsteht aus der jetzigen Dev-DB),
kommt die Altwelt komplett raus.

Laufzeit-Code (Schritt 3a):
- ledger.php: Option legacy_mitarbeiter_ids, die Spalten aus allen SELECTs und
  Rueckgaben, ledger_fetch_participant_summary_by_legacy_id sowie das Loeschen
  gespiegelter Legacy-Zeilen in ledger_void_entry/ledger_void_own_self_entry
  entfernt
- imports.php, paypal-inbox.php: legacy_mitarbeiter_id aus Ergebnis und
  Typannotationen entfernt
- ledger-preview.php: Spalte "Legacy" entfernt

Skripte (Schritt 3b/3c):
- die acht reinen Legacy-/Golden-Master-Skripte entfernt (backfill-*,
  seed-golden-master, golden-master-data, check-golden-master, check-m4-*,
  check-m3-saas-basis)
- init-mysql-dev.php legt jetzt einen SaaS-Mandanten mit Owner-Login,
  Teilnehmer, Journalbuchungen und Hinweis an statt kl_*-Zeilen

Schema (Schritt 4, Migration 0024):
- participants.legacy_mitarbeiter_id + uq_participants_tenant_legacy
- ledger_entries.legacy_table/legacy_id + uq_ledger_entries_tenant_legacy
- DROP der Tabellen kl_Einzahlungen, kl_Kaffeeverbrauch, kl_hinweise,
  kl_config, kl_Mitarbeiter

Verifikation unveraendert gruen: http-smoke 33/0, role-matrix 55/0,
tenant-isolation 11/1 (Altfehler), m3-auth/tenant-resolution/billing gruen.
check-m3-settings-flow 7/6 ist vorbestehend (schon bei 3e24910 rot, mit
spaeteren Settings-Feldern nicht synchron) und unabhaengig von diesem Abbau.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 08:31:08 +02:00
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
clemensandClaude Opus 4.8 eec7a2fef2 Legacy-Abbau Schritt 1: Dual-Write in die kl_*-Tabellen entfernt
Die Migration in participants/ledger_entries ist abgeschlossen: kein
Teilnehmer traegt noch eine legacy_mitarbeiter_id, kein Journaleintrag eine
legacy_table. Damit war der jeweils zweite Zweig ("Default-Mandant schreibt
zusaetzlich nach kl_Einzahlungen/kl_Kaffeeverbrauch/kl_Mitarbeiter") in
sechs Dateien nicht mehr erreichbar - er musste aber bei jeder Aenderung
mitgepflegt werden, zuletzt bei der Bemerkung fuer Einzahlungen.

Entfernt:
- Dual-Write beim Buchen: einzahlung.php, stricheintragen.php, index.php,
  jahresauswertung.php, app/imports.php, app/paypal-inbox.php
- Dual-Write in der Mitgliederverwaltung: ledger_create_participant,
  ledger_update_participant, ledger_set_participant_active,
  ledger_anonymize_participant
- die verwaisten Spiegelfunktionen ledger_mirror_legacy_payment,
  ledger_mirror_legacy_consumption und ledger_void_entry_by_legacy_id

Nebenbei behoben: kaffeeliste.php hat die Teilnehmer-Detailseite nur fuer
Mitglieder mit legacy_mitarbeiter_id verlinkt - die hat seit der Migration
niemand mehr, die Seite war also fuer alle unerreichbar. Verlinkt und
adressiert wird jetzt ueber participant_id; "user_id" bleibt als Alias
erhalten. Der Mandanten-Isolationstest prueft jetzt ledger_void_entry, also
den Pfad, den letzteneintraege.php tatsaechlich nutzt.

Pruefskripte unveraendert gegenueber der Baseline vor dem Umbau
(http-smoke 27/6, role-matrix 55/0); die Isolationspruefung steigt von
9 auf 11 PASS bei gleichem vorbestehendem Fehler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:57:49 +02:00
clemensandClaude Opus 4.8 dd6e1e77b0 Bemerkung und Abzuege bei Einzahlungen
Einzahlungen koennen jetzt eine Bemerkung tragen und negativ sein, damit
Abzuege (Auszahlung bei Austritt, Erstattung) nachvollziehbar gebucht werden
koennen.

- ledger_entries.note wurde bisher nur beim Import befuellt und wird nun auch
  bei Einzahlungen geschrieben und angezeigt
- Negative Betraege verlangen zwingend eine Bemerkung, sonst ist spaeter nicht
  mehr nachvollziehbar, warum jemandem Geld abgezogen wurde
- Plausibilitaetsgrenze von 1000 EUR gegen Groessenordnungs-Tippfehler; bei
  einem Fehler in einer Zeile wird gar nichts gebucht und die Eingaben bleiben
  stehen
- Legacy-Tabelle kl_Einzahlungen bekommt eine Bemerkung-Spalte, sonst haette
  ledger_mirror_legacy_payment die Notiz beim Re-Sync wieder mit NULL
  ueberschrieben
- PayPal-Buchungen tragen jetzt Zahler, Datum und Mitteilung als Bemerkung

Ausserdem behoben: letzteneintraege.php hat Einzahlungen und Striche direkt
aus den Legacy-Tabellen gelesen, ohne nach Mandant zu filtern. Jeder
Mandanten-Admin sah damit die Buchungen aus den Legacy-Tabellen, und das
Stornieren lief ueber die nicht mandantengetrennten Legacy-IDs. Die Seite
arbeitet jetzt auf dem mandantengebundenen Journal, und ledger_void_entry
prueft Mandant und Buchungsart, bevor es storniert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:53:10 +02:00
clemensandClaude Opus 4.8 ea7d9a4714 Automatische Verbuchung von PayPal-Zahlungen per Mail-Weiterleitung
Mandanten leiten ihre PayPal-Benachrichtigungsmails an ein zentrales
IMAP-Postfach weiter. Die Zuordnung zum Mandanten erfolgt ueber
Plus-Adressierung (zahlungen+<token>@...), der Token wird pro Mandant
erzeugt und im Backend angezeigt.

- Parser fuer PayPal-Eingangsmails, tolerant gegenueber falsch kodierten
  Waehrungssymbolen und HTML-Struktur weitergeleiteter Mails
- Gutgeschrieben wird der Nettobetrag, also was tatsaechlich ankam
  (bei Waren & Dienstleistungen nach Abzug der PayPal-Gebuehr)
- Deduplizierung ueber den Transaktionscode, damit doppelt weitergeleitete
  Mails nicht doppelt buchen
- Eindeutiger Namens-Match bucht automatisch, alles andere landet in einer
  Warteschlange zur manuellen Zuordnung
- Absenderpruefung gegen paypal.de/.com, da weitergeleitete Mails keine
  gueltige SPF/DKIM-Signatur mehr haben
- IMAP-Zugang ausschliesslich ueber Server-/Env-Einstellungen, nicht pro
  Mandant konfigurierbar

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:33:49 +02:00
clemens c16b105f00 Erweiterte Bezahloptionen: Barzahlung, PayPal und Ueberweisung
Bisher gab es nur PayPal. Jetzt sind pro Mandant drei unabhaengig
aktivierbare Zahlungswege konfigurierbar (mandant-einstellungen.php):

- Barzahlung mit frei waehlbarem Ansprechpartner.
- PayPal wie bisher, mit Zahlungslink.
- Ueberweisung mit Kontoinhaber und IBAN (Format wird geprueft,
  Leerzeichen werden normalisiert).

Das Mitglieder-Dashboard (index.php) zeigt unter "Bezahlen" alle
aktivierten Methoden mit dem jeweils offenen Betrag; die IBAN wird zur
besseren Lesbarkeit in Viererbloecken angezeigt.

Neue tenant_settings-Spalten via Migration 0021 (cash_enabled,
cash_contact, bank_transfer_enabled, bank_account_holder, bank_iban).
2026-07-21 16:13:28 +02:00
clemens bafa5e7e61 Stripe-Konfigurationsfehler in abo-upgrade.php sauber abfangen
billing_stripe_price_id() und der komplette abo-upgrade.php-Ablauf warfen
bisher eine ungefangene RuntimeException (Fatal Error), sobald
STRIPE_SECRET_KEY nicht gesetzt ist (z. B. lokale Dev-Umgebung ohne echte
Stripe-Zugangsdaten) - bestand bereits im alten Code, ist durch die neue
Tarif-Vergleichstabelle mit mehr Buchen-Buttons aber leichter auslösbar
geworden. Jetzt: sauberer 502 'Bezahldienst nicht verfuegbar' statt
Fatal-Error-Seite.

Erstmals lokal per HTTP end-to-end gegen echten Dev-Server + MariaDB
getestet (PHP 8.3 + MariaDB 11.4 als portable Binaries installiert, siehe
docs/dev-mysql.md): eigener Test-Mandant mit 12 aktiven Teilnehmern,
Tarif-Tabelle korrekt gerendert (5 Tarife, Pflicht-Upgrade-Hinweis, Buchen-
Buttons, Enterprise-Anfrage-Link), Buchung von 'free' bei Ueberschreitung
korrekt mit 400 abgelehnt, Buchung eines bezahlten Tarifs ohne Stripe-Key
jetzt sauber mit 502 statt Fatal-Error-Crash. Alle bestehenden
Regressionstests weiterhin gruen: Golden Master (104), Billing-Capacity
(10), M8-Isolation (10), M8-Rollenmatrix (55).
2026-07-20 20:38:12 +00:00
clemens dd2277c803 Selbstbedienungs-Tarifverwaltung: Admin kann Abo im Backend waehlen und bestellen
- mandant-einstellungen.php: vollstaendige Tarif-Vergleichstabelle statt nur
  erzwungenem Upgrade-Hinweis. Jeder Tarif zeigt Limit, Preis und einen
  Aktions-Button (Buchen/Kuendigen/Anfragen); aktueller Tarif markiert,
  zu kleine Tarife fuer die aktuelle Teilnehmerzahl deaktiviert.
- abo-upgrade.php: unterscheidet jetzt Upgrade, Downgrade zwischen bezahlten
  Stufen (Preis wird in-place gewechselt statt neuer Checkout-Session,
  Stripe prorated automatisch) und Wechsel auf 'free' (echte Kuendigung der
  bestehenden Subscription), statt nur eine Checkout-Session zu erzeugen.
- app/stripe.php: neue Helper stripe_get_subscription(),
  stripe_update_subscription_price(), stripe_cancel_subscription().
- app/billing.php: billing_plan_selectable_for() prueft serverseitig, ob
  ein Zieltarif die aktuelle Teilnehmerzahl noch abdeckt (Downgrade-Schutz).
- docs/billing.md: Phase 4 dokumentiert.

Getestet: php -l fuer alle geaenderten Dateien und das gesamte Repo (keine
Syntaxfehler), reine Funktionslogik-Tests ohne DB (billing_plans(),
billing_required_plan_code(), stripe_flatten_params()) gruen. Kein Live-Test
gegen echte Stripe-Testobjekte moeglich (keine PHP/DB-Laufzeitumgebung
verfuegbar) - vor Go-Live im Stripe-Testmodus nachholen.
2026-07-20 20:19:05 +00:00
clemens ef5d0e1822 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).
2026-07-20 21:54:16 +02:00
clemens c4cb12b26e Landing-Marketing / SEO-Infrastruktur / a11y-Viewport) 2026-07-20 19:46:08 +02:00
clemens 9871401fbc Überarbeitung landing und index 2026-07-20 19:36:22 +02:00
clemens c9ab82e97a PDF Erstellung anpassung 2026-07-20 00:50:29 +02:00
clemens e64370aea0 Anassung des Menü 2026-07-20 00:05:00 +02:00