Files
kaffeekasse-saas/docs/deployment.md
T
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

485 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 1530 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 1020
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).
## 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
- [ ] 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.