/get_oauth_token.php
- * e.g.: http://localhost/phpmailer/get_oauth_token.php
- * * Ensure dependencies are installed with 'composer install'
- * * Set up an app in your Google/Yahoo/Microsoft account
- * * Set the script address as the app's redirect URL
- * If no refresh token is obtained when running this file,
- * revoke access to your app and run the script again.
- */
-
-namespace PHPMailer\PHPMailer;
-
-/**
- * Aliases for League Provider Classes
- * Make sure you have added these to your composer.json and run `composer install`
- * Plenty to choose from here:
- * @see http://oauth2-client.thephpleague.com/providers/thirdparty/
- */
-//@see https://github.com/thephpleague/oauth2-google
-use League\OAuth2\Client\Provider\Google;
-//@see https://packagist.org/packages/hayageek/oauth2-yahoo
-use Hayageek\OAuth2\Client\Provider\Yahoo;
-//@see https://github.com/stevenmaguire/oauth2-microsoft
-use Stevenmaguire\OAuth2\Client\Provider\Microsoft;
-//@see https://github.com/greew/oauth2-azure-provider
-use Greew\OAuth2\Client\Provider\Azure;
-
-if (!isset($_GET['code']) && !isset($_POST['provider'])) {
- ?>
-
-
-
-
-
- $clientId,
- 'clientSecret' => $clientSecret,
- 'redirectUri' => $redirectUri,
- 'accessType' => 'offline'
-];
-
-$options = [];
-$provider = null;
-
-switch ($providerName) {
- case 'Google':
- $provider = new Google($params);
- $options = [
- 'scope' => [
- 'https://mail.google.com/'
- ]
- ];
- break;
- case 'Yahoo':
- $provider = new Yahoo($params);
- break;
- case 'Microsoft':
- $provider = new Microsoft($params);
- $options = [
- 'scope' => [
- 'wl.imap',
- 'wl.offline_access'
- ]
- ];
- break;
- case 'Azure':
- $params['tenantId'] = $tenantId;
-
- $provider = new Azure($params);
- $options = [
- 'scope' => [
- 'https://outlook.office.com/SMTP.Send',
- 'offline_access'
- ]
- ];
- break;
-}
-
-if (null === $provider) {
- exit('Provider missing');
-}
-
-if (!isset($_GET['code'])) {
- //If we don't have an authorization code then get one
- $authUrl = $provider->getAuthorizationUrl($options);
- $_SESSION['oauth2state'] = $provider->getState();
- header('Location: ' . $authUrl);
- exit;
- //Check given state against previously stored one to mitigate CSRF attack
-} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {
- unset($_SESSION['oauth2state']);
- unset($_SESSION['provider']);
- exit('Invalid state');
-} else {
- unset($_SESSION['provider']);
- //Try to get an access token (using the authorization code grant)
- $token = $provider->getAccessToken(
- 'authorization_code',
- [
- 'code' => $_GET['code']
- ]
- );
- //Use this to interact with an API on the users behalf
- //Use this to get a new access token if the old one expires
- echo 'Refresh Token: ', $token->getRefreshToken();
-}
diff --git a/README.md b/README.md
index 267a2b5..724af2d 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,8 @@ Fortschritt je Meilenstein steht in `docs/m2-technical-foundation.md` bis
`scripts/migrate.php`.
- `scripts/`: Migrations-, Backfill- und Prüfskripte (Golden Master,
HTTP-Smoke, M3/M4-Checks).
-- `docs/`: Planungs- und Meilensteindokumentation.
+- `docs/`: Planungs- und Meilensteindokumentation. Ausrollen und
+ Inbetriebnahme auf dem Webspace beschreibt `docs/deployment.md`.
## Umgang mit Legacy-Seiten
diff --git a/docs/deployment.md b/docs/deployment.md
new file mode 100644
index 0000000..5896719
--- /dev/null
+++ b/docs/deployment.md
@@ -0,0 +1,279 @@
+# Deployment und Go-Live
+
+Stand: 2026-08-06
+
+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 | Zweck |
+| --- | --- | --- | --- |
+| Staging | `testumgebung.kaffeeliste.de` | `/testumgebung.kaffeeliste.de/httpdocs/` | Vollständiger Durchlauf vor jedem Produktiv-Deploy |
+| Produktion | `app.kaffeeliste.de` (App) + `kaffeeliste.de` (Marketing) | noch einzurichten | Echtbetrieb |
+
+Beide Umgebungen brauchen **eigene Datenbanken** und **eigene**
+`env.local.php`. Die Staging-Umgebung läuft mit Stripe-Testkeys, die
+Produktion mit Live-Keys.
+
+### 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.
+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 leere MySQL-Datenbank samt eigenem Benutzer anlegen. Die
+Zugangsdaten werden gleich in `env.local.php` eingetragen.
+
+Die vorhandene Entwicklungsdatenbank **nicht** übernehmen: sie enthält
+ausschließlich Testdaten inklusive des migrierten Default-Mandanten aus
+der Legacy-App.
+
+### 3. Dateien hochladen
+
+FTP-Sync ausführen. Danach prüfen, dass folgende URLs **403/404** liefern
+und nicht etwa Inhalt:
+
+```
+https:///sync_config.jsonc
+https:///.env.local
+https:///.git/config
+https:///scripts/migrate.php
+https:///docs/deployment.md
+https:///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('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).
+
+### 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 24 versionierte Migrationen in `database/migrations/`, angewendet
+über `scripts/migrate.php`. Das Skript ist idempotent und wendet nur
+fehlende Migrationen an.
+
+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 |
+| 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 beide PHP-Tasks einmal manuell auslösen.
+`send-payment-reminders.php` unterstützt dafür `--dry-run`.
+
+## 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:///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.
+
+## 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+@…` ankommen. IMAP-Zugangsdaten in
+`env.local.php` eintragen und die `imap`-Extension auf dem Host prüfen.
+
+## 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`:
+
+- [ ] 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
+- [ ] Impressum, AGB und Datenschutzerklärung inhaltlich freigegeben
+- [ ] Auftragsverarbeitungsvertrag (AVV) für Kunden vorbereitet
+- [ ] Frische Produktivdatenbank ohne Testdaten
+
+## 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.