534 lines
24 KiB
Markdown
534 lines
24 KiB
Markdown
# Deployment und Go-Live
|
||
|
||
Stand: 2026-08-22
|
||
|
||
Dieses Dokument beschreibt, wie die Anwendung auf den Netcup-Webspace
|
||
(Plesk) ausgerollt und in Betrieb genommen wird. Es ergänzt
|
||
`docs/betrieb-backup-monitoring.md` (Backup/Monitoring) und
|
||
`docs/betrieb-mail.md` (Mailversand).
|
||
|
||
## Umgebungen
|
||
|
||
| Umgebung | Host | Webroot | Tabellen-Prefix | Zweck |
|
||
| --- | --- | --- | --- | --- |
|
||
| Staging | `testumgebung.kaffeeliste.de` | `/testumgebung.kaffeeliste.de/httpdocs/` | `test_` | Vollständiger Durchlauf vor jedem Produktiv-Deploy |
|
||
| Produktion | `app.kaffeeliste.de` (App) + `kaffeeliste.de` (Marketing) | noch einzurichten | `prod_` | Echtbetrieb |
|
||
|
||
Beide Umgebungen verwenden dieselbe physische Datenbank, aber zwingend
|
||
unterschiedliche Tabellen-Prefixe und jeweils eine eigene `env.local.php`.
|
||
Damit sind Daten, Migrationstabellen und Fremdschlüssel von Staging und
|
||
Produktion logisch getrennt. Die Staging-Umgebung läuft mit Stripe-Testkeys,
|
||
die Produktion mit Live-Keys.
|
||
|
||
Der Prefix schützt vor versehentlichem Zugriff der einen Umgebung auf die
|
||
Tabellen der anderen, ersetzt aber keine getrennte Datenbank als harte
|
||
Sicherheitsgrenze: Ein kompromittierter gemeinsamer Datenbankzugang kann
|
||
weiterhin alle Tabellen sehen. Deshalb Zugangsdaten besonders restriktiv
|
||
behandeln und immer die gesamte Datenbank sichern.
|
||
|
||
### Host-Split beachten
|
||
|
||
`index.php` prüft den Request-Host gegen `APP_HOST`: stimmt er nicht
|
||
überein, wird statt des Dashboards die Landingpage ausgeliefert. Auf
|
||
Staging muss deshalb `APP_HOST=testumgebung.kaffeeliste.de` gesetzt sein,
|
||
sonst kommt man nie ins Dashboard.
|
||
|
||
In Produktion zeigen beide Vhosts (`kaffeeliste.de` und
|
||
`app.kaffeeliste.de`) auf dasselbe Webroot; `APP_HOST=app.kaffeeliste.de`
|
||
sorgt dann automatisch dafür, dass die Marketing-Domain die Landingpage
|
||
und die App-Domain das Dashboard zeigt.
|
||
|
||
## Was deployt wird — und was nicht
|
||
|
||
Der Upload läuft über den FTP-Sync (`sync_config.jsonc`, Umgebung
|
||
`netcup`). Die Ausschlussliste dort ist sicherheitsrelevant und darf nicht
|
||
geleert werden:
|
||
|
||
Nicht ausliefern: `.git`, `.gitignore`, `.claude`, `.local`, `.vscode`,
|
||
`.env.local`, `env.local.php`, `sync_config.jsonc`, `var/`, `docs/`,
|
||
`README.md`.
|
||
|
||
- `.git` wäre sonst komplett herunterladbar (kompletter Quellcode plus
|
||
Historie).
|
||
- `.env.local` enthält die Zugangsdaten der Entwicklungsdatenbank.
|
||
- `sync_config.jsonc` enthält das FTP-Passwort im Klartext.
|
||
- `var/` ist Laufzeitzustand des Servers (Sessions, Uploads) und würde
|
||
vom lokalen Stand überschrieben.
|
||
|
||
`scripts/` und `database/` **werden** mit ausgeliefert — die
|
||
Plesk-Scheduled-Tasks führen sie vom Webspace aus. Per URL sind sie durch
|
||
die `.htaccess` gesperrt.
|
||
|
||
Zusätzlich sperrt die `.htaccess` `var|database|scripts|docs|app|lib`,
|
||
die Vendor-Verzeichnisse `TCPDF|PHPMailer|DataTables`, Dotfile-Ordner
|
||
sowie Konfigurationsdateien und `.sql`/`.md`/`.jsonc`/`.sh`/`.log`-Dateien.
|
||
Im Ordner `legal/` sind ausschließlich datierte `.txt`-Fassungen von AGB,
|
||
Datenschutz, AVV und Widerrufsbelehrung als dauerhafte Downloads erlaubt.
|
||
Sie erzwingt außerdem HTTPS — ohne Redirect käme ein Nutzer per `http` an
|
||
und bekäme eine Session-Cookie ohne `secure`-Flag.
|
||
|
||
## Erstinbetriebnahme einer Umgebung
|
||
|
||
### 1. Hosting-Voraussetzungen prüfen
|
||
|
||
In Plesk unter *PHP-Einstellungen* bzw. über eine temporäre
|
||
`phpinfo()`-Datei kontrollieren:
|
||
|
||
- PHP ≥ 8.1
|
||
- Extensions: `pdo_mysql`, `fileinfo` (CSV-Upload), `openssl`, `imap`
|
||
(nur für den PayPal-Mailabruf nötig)
|
||
- **`allow_url_fopen = On`** — `app/stripe.php` und `app/dolibarr.php`
|
||
sprechen ihre APIs bewusst über PHP-Streams statt cURL an und fallen
|
||
ohne diese Einstellung aus
|
||
- `display_errors = Off`, `log_errors = On` (siehe
|
||
`docs/betrieb-backup-monitoring.md`)
|
||
- Let's-Encrypt-Zertifikat für den Host ausgestellt
|
||
|
||
Die temporäre `phpinfo()`-Datei danach wieder löschen.
|
||
|
||
### 2. Datenbank anlegen
|
||
|
||
In Plesk eine MySQL-Datenbank samt Benutzer anlegen. Beide Umgebungen dürfen
|
||
dieselben Zugangsdaten verwenden; die Trennung erfolgt über
|
||
`DB_TABLE_PREFIX=test_` beziehungsweise `DB_TABLE_PREFIX=prod_`.
|
||
|
||
Unpräfixierte vorhandene Testdaten dürfen ausschließlich mit dem dafür
|
||
vorgesehenen Übernahmeskript nach `test_` kopiert werden. Sie dürfen niemals
|
||
nach `prod_` übernommen werden.
|
||
|
||
### 3. Dateien hochladen
|
||
|
||
FTP-Sync ausführen. Danach prüfen, dass folgende URLs **403/404** liefern
|
||
und nicht etwa Inhalt:
|
||
|
||
```
|
||
https://<host>/sync_config.jsonc
|
||
https://<host>/.env.local
|
||
https://<host>/.git/config
|
||
https://<host>/scripts/migrate.php
|
||
https://<host>/docs/deployment.md
|
||
https://<host>/app/bootstrap.php
|
||
```
|
||
|
||
Liefert eine davon Inhalt, greift die `.htaccess` nicht (z. B. weil
|
||
`AllowOverride` deaktiviert ist) — dann erst weitermachen, wenn das
|
||
geklärt ist.
|
||
|
||
### 4. `env.local.php` anlegen
|
||
|
||
`env.local.example.php` als Vorlage nehmen, ausfüllen und **direkt per
|
||
FTP** als `env.local.php` ins Webroot legen. Die Datei ist bewusst nicht
|
||
im Repository und wird nicht mit deployt, damit ein Deploy sie nie
|
||
überschreibt.
|
||
|
||
Für Staging mindestens:
|
||
|
||
```php
|
||
putenv('APP_ENV=prod'); // 'dev' würde Mails nur ins Log schreiben
|
||
putenv('APP_HOST=testumgebung.kaffeeliste.de');
|
||
putenv('APP_TIMEZONE=Europe/Berlin');
|
||
putenv('APP_DB_DRIVER=mysql');
|
||
putenv('DB_HOST=localhost');
|
||
putenv('DB_NAME=…'); putenv('DB_USER=…'); putenv('DB_PASS=…');
|
||
putenv('DB_TABLE_PREFIX=test_');
|
||
putenv('APP_SESSION_PATH=' . __DIR__ . '/var/sessions');
|
||
putenv('APP_MAIL_TRANSPORT=mail');
|
||
putenv('APP_MAIL_FROM=noreply@kaffeeliste.de');
|
||
```
|
||
|
||
Stripe-, Dolibarr- und IMAP-Werte je Umgebung ergänzen (siehe unten).
|
||
In der produktiven `env.local.php` muss stattdessen
|
||
`DB_TABLE_PREFIX=prod_` stehen. Ein leerer Prefix ist im laufenden Betrieb
|
||
nicht zulässig.
|
||
|
||
### 5. Schreibrechte für `var/`
|
||
|
||
`app_start_session()` legt `var/sessions` bei Bedarf selbst an, braucht
|
||
dafür aber Schreibrechte im Webroot. Nach dem ersten Aufruf prüfen, dass
|
||
`var/sessions` existiert und beschreibbar ist; ebenso `var/uploads` für
|
||
den CSV-Import. Beide sind per `.htaccess` von außen gesperrt.
|
||
|
||
### 6. Migrationen einspielen
|
||
|
||
Es gibt 30 versionierte Migrationen in `database/migrations/`, angewendet
|
||
über `scripts/migrate.php`. Das Skript ist idempotent und wendet nur
|
||
fehlende Migrationen an. Es arbeitet ausschließlich im Prefix aus der
|
||
jeweiligen `env.local.php`; daher müssen Migrationen einmal in Staging und
|
||
einmal in Produktion ausgeführt werden.
|
||
|
||
Für die einmalige Übernahme vorhandener, bisher unpräfixierter Testdaten:
|
||
|
||
```bash
|
||
php scripts/migrate-table-prefix.php --target-prefix=test_ --replace
|
||
```
|
||
|
||
Das Skript migriert zuerst das `test_`-Schema, ersetzt dessen Inhalt
|
||
transaktional durch eine exakte Kopie der unpräfixierten Daten und vergleicht
|
||
pro Tabelle die Zeilenzahl. Die unpräfixierten Quelltabellen bleiben als
|
||
Rückfallmöglichkeit unverändert. Vor dem Lauf trotzdem ein vollständiges
|
||
Datenbank-Backup erstellen.
|
||
|
||
Ohne SSH-Zugang läuft das über **Plesk → Geplante Aufgaben (Scheduled
|
||
Tasks)** als einmalig ausgeführter Task vom Typ *PHP-Skript ausführen*:
|
||
|
||
```
|
||
Skriptpfad: /httpdocs/scripts/migrate.php
|
||
```
|
||
|
||
Der Task muss nach dem Ausführen wieder deaktiviert oder gelöscht werden.
|
||
Die Ausgabe (`Applied migration: …` bzw. `No new migrations.`) lässt sich
|
||
per E-Mail-Benachrichtigung des Tasks kontrollieren.
|
||
|
||
Alternative ohne Scheduled Task: die Migrationsdateien der Reihe nach in
|
||
phpMyAdmin importieren — dann fehlt allerdings der Eintrag in der
|
||
Versionstabelle, den `migrate.php` pflegt. Der Scheduled-Task-Weg ist
|
||
deutlich vorzuziehen.
|
||
|
||
**Nach jedem Deploy mit neuen Migrationen wiederholen.**
|
||
|
||
### 7. Ersten Mandanten anlegen
|
||
|
||
Über `register.php` auf dem konfigurierten Host registrieren. Für
|
||
Plattform-Administrationsrechte (Back-Office über alle Mandanten hinweg)
|
||
anschließend `scripts/grant-platform-admin.php` als einmaligen
|
||
Plesk-Task ausführen.
|
||
|
||
## Wiederkehrende Aufgaben (Plesk Scheduled Tasks)
|
||
|
||
Ohne diese Tasks fehlen Zahlungserinnerungen, die automatische
|
||
PayPal-Verbuchung und — kritisch — die Backups.
|
||
|
||
| Task | Skript / Befehl | Empfohlener Takt |
|
||
| --- | --- | --- |
|
||
| Zahlungserinnerungen | `/httpdocs/scripts/send-payment-reminders.php` | täglich, z. B. 07:00 |
|
||
| PayPal-Mailabruf | `/httpdocs/scripts/fetch-paypal-payments.php` | alle 15–30 Minuten |
|
||
| Technische Löschfristen | `/httpdocs/scripts/purge-expired-operational-data.php` | stündlich |
|
||
| Beendete Verträge löschen | `/httpdocs/scripts/purge-ended-contracts.php` | täglich |
|
||
| Datenbank-Backup | Shell-Befehl, siehe `docs/betrieb-backup-monitoring.md` | täglich nachts |
|
||
|
||
Zum Backup-Task: Das Zielverzeichnis muss **außerhalb** von `httpdocs`
|
||
liegen (z. B. `/backups` auf Vhost-Ebene), sonst wären die Dumps
|
||
öffentlich abrufbar. Zusätzlich mindestens eine Kopie an einen anderen
|
||
Ort übertragen — ein Backup, das nur auf demselben Webspace liegt,
|
||
schützt nicht gegen den Ausfall des Anbieters.
|
||
|
||
Vor der Scharfschaltung die Aufgaben mit Lösch- oder Buchungswirkung zuerst
|
||
manuell mit `--dry-run` auslösen. Dies unterstützen
|
||
`send-payment-reminders.php`, `fetch-paypal-payments.php`,
|
||
`purge-expired-operational-data.php` und `purge-ended-contracts.php`.
|
||
|
||
## Mailversand und DNS
|
||
|
||
Der Versand läuft über PHPs `mail()` (`APP_MAIL_TRANSPORT=mail`, siehe
|
||
`docs/betrieb-mail.md`). Das betrifft Registrierungsbestätigung,
|
||
E-Mail-Verifikation, Passwort-Reset, Mitglieder-Einladungen,
|
||
Zahlungserinnerungen und den Massenmailversand — also den kompletten
|
||
Onboarding-Pfad.
|
||
|
||
Vor Go-Live für die Absenderdomain einrichten und prüfen:
|
||
|
||
- **SPF**-Eintrag, der den Mailserver des Webspace autorisiert
|
||
- **DKIM**-Signierung (in Plesk pro Domain aktivierbar)
|
||
- **DMARC**-Eintrag
|
||
- `APP_MAIL_FROM` auf eine echte Adresse der eigenen Domain setzen —
|
||
eine fremde Absenderdomain bricht SPF/DKIM
|
||
|
||
Testen mit einer Registrierung auf eine externe Adresse (Gmail/Outlook)
|
||
und Kontrolle, ob die Mail im Posteingang statt im Spam landet.
|
||
Fehlversuche stehen in der Tabelle `outbound_emails` mit
|
||
`status = 'failed'`.
|
||
|
||
## Stripe und Dolibarr
|
||
|
||
Details zur Funktionsweise stehen in `docs/billing.md`. Für den Go-Live:
|
||
|
||
1. **Staging** mit Stripe-**Test**-Keys betreiben und den kompletten
|
||
Ablauf einmal durchspielen: Checkout, Upgrade, Downgrade, Kündigung,
|
||
Customer Portal.
|
||
2. Webhook-Endpunkt im Stripe-Dashboard auf
|
||
`https://<host>/stripe-webhook.php` registrieren (je Umgebung einen
|
||
eigenen) und das dort erzeugte Signing Secret als
|
||
`STRIPE_WEBHOOK_SECRET` eintragen. Ohne gültiges Secret weist
|
||
`stripe-webhook.php` alle Ereignisse ab.
|
||
3. Für Produktion auf **Live**-Keys wechseln und den Webhook erneut
|
||
registrieren — Test- und Live-Modus haben getrennte Webhooks und
|
||
getrennte Secrets.
|
||
4. Dolibarr: `DOLIBARR_URL` und `DOLIBARR_API_KEY` eintragen. Achtung,
|
||
das ist die **produktive** Buchhaltungsinstanz des Kunden, es gibt
|
||
keine Sandbox. Der `validate()`-Aufruf beim Rechnungsabschluss ist
|
||
laut `docs/billing.md` bewusst nie live getestet worden — der erste
|
||
echte Zahlungseingang ist dessen erster Test. Danach in Dolibarr
|
||
kontrollieren, ob die Rechnung korrekt angelegt und validiert wurde.
|
||
|
||
Der Link „Zahlungsmethode verwalten“ startet technisch ausschließlich einen
|
||
Stripe-Flow zur Aktualisierung der Zahlungsart. Im Stripe-Dashboard dürfen der
|
||
öffentliche No-Code-Portal-Login und Tarifwechsel außerhalb der Anwendung nicht
|
||
zusätzlich freigeschaltet werden; Bestellungen und Kündigungen sollen nur über
|
||
die dokumentierten Abläufe der Kaffeeliste erfolgen.
|
||
|
||
## Zwei-Faktor-Anmeldung
|
||
|
||
Jedes Konto kann unter `zwei-faktor-einrichten.php` (verlinkt aus
|
||
`konto.php`) einen zweiten Faktor per TOTP einrichten — dieselben
|
||
sechsstelligen Codes, die Aegis, 2FAS, Google Authenticator oder ein
|
||
Passwortmanager erzeugen.
|
||
|
||
`app/totp.php` implementiert RFC 6238 selbst, statt eine Bibliothek zu
|
||
ziehen: der Algorithmus ist ein HMAC plus eine Truncation, und ein zweiter
|
||
Faktor ist die letzte Stelle, an der man ungeprüfte Abhängigkeiten haben
|
||
will. Der QR-Code entsteht aus dem ohnehin vorhandenen TCPDF — kein
|
||
externer Dienst bekommt das Geheimnis zu sehen.
|
||
|
||
Ablauf beim Login: stimmt das Passwort und ist ein zweiter Faktor scharf,
|
||
wird die Anmeldung **nicht** abgeschlossen. Es entsteht nur ein
|
||
Zwischenzustand (`totp_pending_user_id`, 15 Minuten gültig), der für sich
|
||
genommen keinerlei Zugriff gewährt; erst der Code stellt die Sitzung her.
|
||
Wer mehrere Mandanten hat, kommt auch erst danach zur Mandantenauswahl.
|
||
|
||
Weitere Festlegungen:
|
||
|
||
- **Wiederherstellungscodes**: zehn Stück, nur direkt nach der Einrichtung
|
||
im Klartext sichtbar, gespeichert als Hash, jeder genau einmal gültig.
|
||
Auf der Code-Seite genügt es, statt des App-Codes einen davon einzugeben —
|
||
Buchstaben im Feld unterscheiden die beiden Fälle.
|
||
- **Wiederverwendung ausgeschlossen**: `users.totp_last_step` merkt sich den
|
||
zuletzt eingelösten Zeitschritt. Ohne das ließe sich ein abgefangener Code
|
||
innerhalb seines Gültigkeitsfensters ein zweites Mal verwenden.
|
||
- **Toleranz** von einem Zeitschritt (±30 s) für Uhrenabweichungen.
|
||
- **Rate-Limit** auf der Code-Eingabe: 5 Versuche pro Konto und 20 pro IP je
|
||
15 Minuten — sechs Ziffern sind sonst schnell durchprobiert.
|
||
- **Abschalten** verlangt Passwort *und* gültigen Code.
|
||
|
||
`APP_REQUIRE_2FA_FOR_ADMINS=1` macht den zweiten Faktor für Platform-Admins
|
||
verbindlich: das Back-Office leitet dann auf die Einrichtung um, statt hart
|
||
zu sperren — sonst könnte der Schalter den einzigen Platform-Admin dauerhaft
|
||
aussperren. **Erst umlegen, nachdem die eigene Einrichtung getestet ist.**
|
||
|
||
Abgedeckt von `scripts/check-totp-flow.php` (Algorithmus inklusive der
|
||
RFC-6238-Testvektoren, Persistenz, Wiederherstellungscodes) und
|
||
`scripts/check-2fa-http-flow.php` (Anmeldeweg; braucht einen laufenden
|
||
Webserver).
|
||
|
||
## Schutz der offenen Registrierung
|
||
|
||
Ab Juli 2026 liefen auf `testumgebung.kaffeeliste.de` täglich 10–20
|
||
automatisierte Registrierungen: zufällige Namen kombiniert mit **fremden,
|
||
echten** E-Mail-Adressen. Der Server verschickte daraufhin Vertrags- und
|
||
Verifikationsmail an Leute, die nie etwas bestellt hatten — Mail-Bombing
|
||
über die eigene Domain, auf Kosten der Zustellbarkeit. Das IP-Rate-Limit
|
||
griff nicht, weil jede Anfrage über eine eigene Rechenzentrums-IP kam.
|
||
|
||
`app/spam-guard.php` stellt deshalb vor `register.php`:
|
||
|
||
- **Honigtopf** — ein für Menschen unsichtbares Feld (`contact_reference`),
|
||
das Formular-Bots ausfüllen. Die Positionierung steht bewusst inline und
|
||
nicht in `public.css`: lädt das Stylesheet einmal nicht, wäre das Feld
|
||
sonst sichtbar und ein echter Nutzer könnte hineinschreiben.
|
||
- **Zeitfalle** — mindestens vier Sekunden zwischen Ausliefern und
|
||
Absenden des Formulars.
|
||
- **Globale Notbremse** — höchstens 20 Registrierungen pro Stunde über
|
||
alle IPs hinweg, zusätzlich zum bestehenden Limit von 5 pro IP.
|
||
- **Domain-Prüfung** — die Mail-Domain muss einen MX- oder A/AAAA-Record
|
||
haben. Fehlt `checkdnsrr()`, wird durchgelassen: eine kaputte
|
||
DNS-Auflösung darf keine echten Registrierungen blockieren.
|
||
|
||
Honigtopf, Zeitfalle und beide Rate-Limits antworten mit **derselben**
|
||
generischen Meldung, damit die Antwort nicht verrät, welche Hürde
|
||
angeschlagen hat. Treffer stehen im PHP-Error-Log (`Spam-Guard: …`) — dort
|
||
lässt sich ablesen, ob die Maßnahmen greifen. Nur die Domain-Prüfung nennt
|
||
den Grund, weil das in aller Regel ein Tippfehler eines echten Nutzers ist.
|
||
|
||
Bewusst **kein** Captcha: Turnstile oder hCaptcha wären wirksam, holen aber
|
||
einen Drittanbieter in den Registrierungspfad und damit in die
|
||
Datenschutzerklärung. Erst wenn die obigen Hürden nachweislich nicht
|
||
reichen, ist das die nächste Stufe.
|
||
|
||
Abgedeckt von `scripts/check-spam-guard.php` (ohne Datenbank lauffähig).
|
||
|
||
## Matomo und Analyse-Einwilligung
|
||
|
||
Die Anwendung enthält eine eigene Einwilligungsoberfläche und lädt Matomo
|
||
erst nach „Analyse erlauben“. Ohne vollständige Konfiguration wird weder die
|
||
Oberfläche ausgegeben noch eine Verbindung zu Matomo aufgebaut. Für die
|
||
Produktion in `env.local.php` ergänzen:
|
||
|
||
```php
|
||
putenv('MATOMO_URL=https://analytics.kaffeeliste.de/');
|
||
putenv('MATOMO_SITE_ID=1');
|
||
putenv('MATOMO_CONSENT_COOKIE_DOMAIN=.kaffeeliste.de');
|
||
putenv('MATOMO_PRIVACY_CONFIRMED=1');
|
||
```
|
||
|
||
`MATOMO_SITE_ID` durch die numerische Website-ID ersetzen. Die gemeinsame
|
||
Cookie-Domain ist nur richtig, wenn Marketingseite, App und Matomo tatsächlich
|
||
unter `kaffeeliste.de` betrieben werden sollen. Sonst leer lassen; die
|
||
Einwilligung gilt dann nur auf dem jeweils besuchten Host.
|
||
|
||
`MATOMO_PRIVACY_CONFIRMED=1` erst setzen, nachdem die Matomo-Instanz so
|
||
konfiguriert und geprüft wurde:
|
||
|
||
1. Matomo wird selbst auf der im Datenschutzhinweis genannten EU-Infrastruktur
|
||
betrieben; Matomo Cloud oder ein anderer Empfänger ist nicht eingetragen.
|
||
2. Unter Datenschutz/Anonymisierung werden IP-Adressen vor Speicherung um
|
||
mindestens zwei Bytes gekürzt.
|
||
3. Roh- und Besuchsprotokolle werden spätestens nach 180 Tagen automatisch
|
||
gelöscht; verdichtete Berichte spätestens nach 25 Monaten.
|
||
4. Keine User-ID, benutzerbezogene Custom Dimensions, Heatmaps,
|
||
Sitzungsaufzeichnungen oder Werbeintegrationen aktivieren. Der eingebundene
|
||
Tracker deaktiviert zusätzlich Browser-Funktionserkennung,
|
||
Performance-Messung und Link-Klick-Tracking.
|
||
5. In Matomo ist genau eine Website mit den erlaubten Marketing- und
|
||
App-Domains angelegt. Testaufrufe mit Query-Parametern dürfen in Matomo nur
|
||
den Pfad ohne Query oder Fragment zeigen.
|
||
6. Im privaten Browserfenster prüfen: Vor der Entscheidung und nach „Nur
|
||
notwendige“ gibt es keine Anfrage an `matomo.js`/`matomo.php` und keine
|
||
`_pk_*`-Cookies. Nach Zustimmung gibt es beides; ein anschließender Widerruf
|
||
über „Cookie-Einstellungen“ stoppt weitere Aufrufe und löscht die Cookies.
|
||
|
||
Die Entscheidung wird 180 Tage im Cookie
|
||
`kaffeeliste_analytics_consent` gespeichert. Eine Versionsänderung in
|
||
`APP_ANALYTICS_CONSENT_VERSION` fragt erneut. Änderungen an Zweck, Datenumfang
|
||
oder Empfängern erfordern außerdem eine neue Datenschutzerklärung und eine
|
||
erneute rechtliche Prüfung.
|
||
|
||
## Betreiber-Benachrichtigung bei neuer Registrierung
|
||
|
||
`register.php` schickt nach einer erfolgreichen Selbstregistrierung einen
|
||
Pushover-Push an den Betreiber (`app/pushover.php`). Dafür in
|
||
`env.local.php` `PUSHOVER_TOKEN` (App-Token aus dem Pushover-Dashboard) und
|
||
`PUSHOVER_USER` (eigener User- oder Group-Key) setzen. Fehlt einer der
|
||
beiden Werte, ist die Benachrichtigung still deaktiviert — auf Staging also
|
||
einfach leer lassen.
|
||
|
||
Der Versand ist bewusst folgenlos: Schlägt er fehl, landet die Ursache im
|
||
PHP-Error-Log, die Registrierung läuft trotzdem durch. Der Aufruf sitzt
|
||
deshalb in `register.php` und nicht in `saas_register_tenant_owner()` — die
|
||
`check-*`-Skripte in `scripts/` registrieren Testmandanten und würden sonst
|
||
bei jedem Lauf Pushes auslösen.
|
||
|
||
## PayPal-Postfach
|
||
|
||
Für die automatische Verbuchung weitergeleiteter PayPal-Zahlungsmails
|
||
muss das Postfach aus `PAYPAL_INBOX_BASE` (Vorgabe
|
||
`zahlungen@kaffeeliste.de`) existieren und **Plus-Adressierung an
|
||
dieselbe Mailbox zustellen** (Catch-All), damit die pro Mandant
|
||
generierten Adressen `zahlungen+<token>@…` ankommen. IMAP-Zugangsdaten in
|
||
`env.local.php` eintragen und die `imap`-Extension auf dem Host prüfen.
|
||
|
||
### Erreichbarkeit prüfen
|
||
|
||
`scripts/check-imap-support.php` beantwortet in einem Lauf, ob dieser Host
|
||
das Postfach erreichen kann: PHP-Version, ob die `imap`-Erweiterung
|
||
geladen ist, ob die `PAYPAL_*`-Einstellungen gesetzt sind, und ob sich das
|
||
Postfach **auch ohne die Erweiterung** über eine reine TLS-Verbindung
|
||
öffnen lässt. Der Test ist nur lesend (`EXAMINE`, keine Flags), das
|
||
Passwort wird nie ausgegeben.
|
||
|
||
Fehlt die Erweiterung: In Plesk unter *PHP-Einstellungen* prüfen, ob
|
||
`imap` angeboten wird — nachinstallieren kann sie nur der Anbieter
|
||
(`plesk-php8x-imap`). Seit **PHP 8.4** ist `imap` kein Bestandteil von PHP
|
||
mehr (nach PECL ausgelagert); auf 8.4/8.5 steht sie deshalb oft gar nicht
|
||
mehr zur Wahl, dann hilft ein PHP-Handler auf 8.3 — oder ein Abruf über
|
||
die TLS-Verbindung, die das Diagnoseskript bereits nachweist.
|
||
|
||
### Erster Testlauf
|
||
|
||
`scripts/fetch-paypal-payments.php --dry-run` fasst nichts an: es meldet
|
||
Verbindung, erkannten Mandanten und was mit jeder Mail passieren *würde*
|
||
(`would_book` / `would_queue` / `would_park` / `duplicate`), speichert
|
||
aber nichts, bucht nichts und markiert keine Mail als gelesen. Erst der
|
||
Lauf ohne `--dry-run` verbucht.
|
||
|
||
Reihenfolge für die Inbetriebnahme:
|
||
|
||
1. Eingangsadresse des Mandanten holen — sie steht auf
|
||
`paypal-zuordnung.php` („PayPal-Zahlungen").
|
||
2. Eine echte PayPal-Zahlungsmail dorthin weiterleiten.
|
||
3. Task mit `--dry-run` auslösen, Ausgabe kontrollieren.
|
||
4. Task ohne `--dry-run` auslösen, Ergebnis in der App prüfen
|
||
(Journal-Vorschau bzw. Warteschlange).
|
||
5. Erst danach den Task auf den regelmäßigen Takt stellen.
|
||
|
||
Ohne IMAP lässt sich die Verarbeitung auch mit einer gespeicherten Mail
|
||
prüfen:
|
||
|
||
```
|
||
php scripts/fetch-paypal-payments.php --file=mail.html \
|
||
--recipient=zahlungen+<token>@kaffeeliste.de [--dry-run]
|
||
```
|
||
|
||
Die Verarbeitungskette selbst (Token, Parser, Zuordnung, Buchung, Dedup,
|
||
Parken) deckt `scripts/check-paypal-inbox-flow.php` als
|
||
Regressionstest ab — alles außer der IMAP-Verbindung.
|
||
|
||
## Deploy-Ablauf für ein Update
|
||
|
||
1. Lokal committen und pushen.
|
||
2. FTP-Sync ausführen (Ausschlussliste greift automatisch).
|
||
3. Falls neue Migrationen dabei sind: Migrations-Task in Plesk einmal
|
||
auslösen.
|
||
4. Stichprobe: Login, Dashboard, eine Strich-Buchung, PDF-Export.
|
||
|
||
Ein echtes Rollback gibt es nicht; im Fehlerfall wird der vorherige
|
||
Commit-Stand erneut hochgeladen. Migrationen sind nicht rückwärts
|
||
lauffähig — vor einem Deploy mit Schemaänderungen daher immer ein
|
||
frisches Backup ziehen.
|
||
|
||
## Go-Live-Checkliste
|
||
|
||
Vor der Umstellung auf `app.kaffeeliste.de`:
|
||
|
||
Zuerst aus dem Projektverzeichnis ausführen:
|
||
|
||
```
|
||
php scripts/check-production-readiness.php
|
||
```
|
||
|
||
Der Check blockiert unter anderem bei fehlender B2C-Telefonnummer,
|
||
unsicherer Basis-URL, fehlenden Zahlungs-/Abrechnungsschlüsseln, zu offenen
|
||
Rechten der Secret-Dateien oder einer nicht angewendeten Legal-Migration.
|
||
|
||
- [ ] Kompletter Funktionsdurchlauf auf Staging erfolgreich
|
||
(Registrierung, Mailversand, Mitglieder, Striche, Einzahlungen,
|
||
CSV-Import, PDF-Export, Jahresauswertung, Stripe im Testmodus)
|
||
- [ ] Die sechs Abruf-Tests aus Schritt 3 liefern alle 403/404
|
||
- [ ] `display_errors` produktiv aus, `log_errors` an
|
||
- [ ] SPF/DKIM/DMARC gesetzt, Testmail landet nicht im Spam
|
||
- [ ] Backup-Task läuft und ein Restore wurde einmal testweise
|
||
eingespielt (`docs/betrieb-backup-monitoring.md`)
|
||
- [ ] Stripe-Live-Keys und Produktiv-Webhook eingetragen
|
||
- [ ] `LEGAL_PHONE`, `LEGAL_EMAIL` und `LEGAL_TICKET_URL` zeigen auf
|
||
tatsächlich betreute Kontaktwege; Tickets werden werktags regelmäßig
|
||
bearbeitet
|
||
- [ ] Impressum, AGB, Datenschutz, AVV und Widerrufsbelehrung inhaltlich
|
||
anwaltlich für B2B und B2C freigegeben
|
||
- [ ] Kündigungs- und Widerrufsformular erzeugen beim Kunden und bei
|
||
`LEGAL_EMAIL` eine E-Mail; die Textbestätigung lässt sich speichern
|
||
- [ ] Offene Rechtserklärungen werden täglich im Back-Office geprüft;
|
||
Rückzahlungen nach Widerruf werden manuell über Stripe fristgerecht
|
||
bearbeitet (die Anwendung beendet das Abo, erstattet aber nicht automatisch)
|
||
- [ ] Stripe Checkout läuft auf Deutsch, zeigt Endpreis und Monatslaufzeit;
|
||
Test von Neubuchung, Tarifwechsel und Kündigung zum Periodenende
|
||
- [ ] Webserver-Access-Logs werden spätestens nach sieben Tagen gelöscht oder
|
||
anonymisiert; TLS/HSTS ist auf allen öffentlichen Domains aktiv
|
||
- [ ] Falls Matomo aktiv ist: Datenschutz-Checkliste oben vollständig geprüft,
|
||
Opt-in/Ablehnung/Widerruf im Browser getestet und
|
||
`MATOMO_PRIVACY_CONFIRMED=1` gesetzt
|
||
- [ ] Täglicher Task `php scripts/purge-ended-contracts.php` ist eingerichtet;
|
||
ein Vorlauf mit `--dry-run` wurde kontrolliert
|
||
- [ ] Stündlicher Task `php scripts/purge-expired-operational-data.php` ist
|
||
eingerichtet; ein Vorlauf mit `--dry-run` wurde kontrolliert
|
||
- [ ] Frisches `prod_`-Schema ohne Testdaten; Staging verwendet ausschließlich `test_`
|
||
|
||
## Bewusst offen
|
||
|
||
- **Content-Security-Policy**: nicht gesetzt, weil die Templates
|
||
durchgängig Inline-Styles nutzen. Begründung und Umfang in
|
||
`docs/m8-haertung.md`.
|
||
- **Automatisiertes Deployment** (CI/CD, Build-Schritt): nicht
|
||
eingerichtet. Bei einer flachen PHP-App ohne Build-Prozess und einem
|
||
einzelnen Deployer ist der FTP-Sync angemessen.
|
||
- **`DataTables/` und `js/`**: liegen im Repo, werden aber von keiner
|
||
Seite mehr eingebunden. Sie sind per `.htaccess` gesperrt und können
|
||
bei Gelegenheit entfernt werden.
|