refactor(mail): send via the SSO app registration, drop SMTP basic auth
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m25s
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m25s
Mail now uses the same Entra ID app registration as "Sign in with Microsoft" (ENTRA_TENANT_ID, ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET), so only one enterprise application is needed. The app additionally gets the SMTP.SendAsApp permission; Exchange still limits it to the mailbox granted via Add-MailboxPermission. Password (basic auth) sending is removed entirely: SMTP_HOST, SMTP_PORT, SMTP_PASSWORD and the separate SMTP_OAUTH_* settings are gone. The transport is fixed to smtp.office365.com:587 with required STARTTLS. SMTP_USER (sending mailbox) and SMTP_FROM remain. - lib/entra.ts exposes readEntraAppCredentials(), shared by SSO and mail - diagnosis drops basic-auth-only causes; hints name the ENTRA_* values and warn that an expired secret breaks sign-in and mail together - admin e-mail test, .env.example, docker-compose, k8s manifests, preflight and README updated Verified against Entra ID with fake values: token error classified, client secret never exposed, missing ENTRA_CLIENT_SECRET reported. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
0620e9d79a
commit
11910dd6dd
15 files changed
+211
-309
No files matched your search
@@ -40,7 +40,7 @@ Regionale Imkerei-Website und Shop für Wabenhain (Altenmünster) — MVP-Stufe.
|
||||
- **Kundenkonto** unter `/konto`: Registrierung mit **Pflicht-E-Mail-Verifizierung** (siehe „Sicherheit" unten) und Login (Passwort-Hash + signiertes Session-Cookie), optionale **Zwei-Faktor-Authentifizierung** (TOTP-Authenticator-App + Backup-Codes, `/konto/zwei-faktor`), Konto-Sperre nach Fehlversuchen, "Alle Geräte abmelden", "Passwort vergessen"-Link mit E-Mail-Reset, Bestellhistorie mit Versand-/Abholstatus, "Bestellung erneut auslösen", Adressverwaltung (mehrere Adressen, Standardadresse), **Wunschliste**, Rechnungsansicht als HTML und PDF-Download (`/rechnung/[nummer]`), Newsletter-Verwaltung mit Double-Opt-In
|
||||
- **Admin-Backend** unter `/admin` (Zugriff über das normale Kundenkonto-Login + Rolle `ADMIN`, siehe „Admin-Zugang" unten): Dashboard mit Kennzahlen, Top-Produkten, Umsatz nach Kategorie, **Umsatzverlauf der letzten 14 Tage**, **Stammkund:innen-Zähler** und **meistgenutzten Gutscheinen**, Bestellliste mit Detailseite (`/admin/bestellungen/[id]`: vollständige Adresse, Artikel, Zahlung, Rabatt, Rechnungs-PDF) inkl. Status-Änderung (setzt automatisch Versand-/Abholdatum **und verschickt eine Versand-/Abholbereit-E-Mail**), Produkte/Ernten/Chargen jeweils mit Anlegen- und Bearbeiten-Formularen, Firmenanfragen-Inbox mit Statuspflege, **Benutzerverwaltung** (`/admin/benutzer`, siehe „Admin-Zugang" unten), **Versandkosten-Verwaltung** (`/admin/versand`, siehe „Versandkosten" unten), **Gutschein-Verwaltung** (`/admin/gutscheine`), **Bewertungsmoderation** (`/admin/bewertungen`: freischalten oder löschen), **Audit-Log** (`/admin/audit-log`, siehe „Sicherheit" unten)
|
||||
- **ApiCore-Synchronisierung**: Ernten, Chargen und der Produktkatalog lassen sich per Knopfdruck aus dem Imkerei-Verwaltungssystem ApiCore übernehmen, statt dieselben Daten zweimal einzutippen — jeweils in einen Entwurfszustand, den erst ein Mensch freigibt, und ohne je etwas zu überschreiben, was im Admin bewusst gesetzt wurde (siehe „ApiCore-Synchronisierung" unten)
|
||||
- **Transaktions-E-Mails** (Bestellbestätigung, Versand-/Abholbereit-Benachrichtigung, E-Mail-Verifizierung, Newsletter-Opt-in, Passwort-Reset) über SMTP, siehe Abschnitt „E-Mail-Versand" unten
|
||||
- **Transaktions-E-Mails** (Bestellbestätigung, Versand-/Abholbereit-Benachrichtigung, E-Mail-Verifizierung, Newsletter-Opt-in, Passwort-Reset) über Microsoft 365 mit OAuth 2.0 (dieselbe App wie die Microsoft-Anmeldung), siehe Abschnitt „E-Mail-Versand" unten
|
||||
- `sitemap.xml`, `robots.txt`, OpenGraph-Metadaten, Favicon (`app/icon.svg`) mit dem echten Wabenhain-Logo
|
||||
- **Barrierefreiheit (WCAG AA)**: die Honigfarbe gibt es jetzt als zwei kontraststarke Varianten (`text-honey-dark` für helle, `text-honey-light` für dunkle Abschnitte wie `bg-forest`) statt eines einzelnen Tons, der auf keinem der beiden Hintergründe zuverlässig 4.5:1 erreichte; dieselbe Kontrastprüfung wurde site-weit für alle `text-ink/NN`-Texttöne nachgezogen (Admin-Bereich, Kundenkonto, Checkout, Rechnungen, Produktkarten, Geschmacksskala — 190+ Stellen in 50 Dateien) sowie für die Bildergalerie auf der Produktseite korrigiert (Vorschaubild-Buttons hatten einen `aria-label`, der den sichtbaren Bildtext überschrieben statt eingeschlossen hat). Lighthouse-Accessibility-Audit steht jetzt bei 100/100 ohne offene Verstöße auf Start-, Shop- und Produktseite.
|
||||
- **Performance**: GSAP (inkl. ScrollTrigger-Plugin) wurde als Animationsbibliothek entfernt und durch `motion/react` ersetzt, das ohnehin schon für andere UI-Animationen geladen wird — spart ein komplettes zweites Animations-Framework (~45 KB gzip) ohne sichtbaren Unterschied im Scroll-Reveal- oder Hero-Effekt. Core-Web-Vitals-Messung (LCP/CLS) zeigt keine Auffälligkeiten.
|
||||
@@ -258,34 +258,28 @@ Die Antwort wird eine Stunde zwischengespeichert (`revalidate: 3600`), was die K
|
||||
|
||||
## E-Mail-Versand
|
||||
|
||||
Bestellbestätigung, Versand-/Abholbereit-Benachrichtigung, E-Mail-Verifizierung, Newsletter-Opt-in und Passwort-Reset werden über SMTP versendet (`src/lib/mail.ts`, `nodemailer`). Ohne `SMTP_HOST`/`SMTP_USER`/`SMTP_PASSWORD` in `.env` wird nicht gesendet, sondern nur geloggt — praktisch für lokale Entwicklung, aber vor Live-Gang unbedingt konfigurieren.
|
||||
Bestellbestätigung, Versand-/Abholbereit-Benachrichtigung, E-Mail-Verifizierung, Newsletter-Opt-in und Passwort-Reset gehen über **Microsoft 365 mit OAuth 2.0** (`src/lib/mail.ts`, `nodemailer`, SMTP mit XOAUTH2 an `smtp.office365.com:587`). Als App dient **dieselbe App-Registrierung wie „Mit Microsoft anmelden“** (`ENTRA_TENANT_ID`, `ENTRA_CLIENT_ID`, `ENTRA_CLIENT_SECRET`) — eine Unternehmensanwendung statt zwei. Eine Anmeldung per Passwort (Basic Auth) gibt es nicht: kein Benutzerpasswort im Cluster, kein Konflikt mit MFA oder Sicherheitsstandards, nicht betroffen vom Ende der Basic Auth bei Exchange Online. Ohne `SMTP_USER` und die `ENTRA_*`-Werte wird nicht gesendet, sondern nur geloggt — praktisch für lokale Entwicklung, aber vor Live-Gang unbedingt einrichten.
|
||||
|
||||
Funktioniert mit jedem SMTP-Anbieter, z.B. dem E-Mail-Postfach eures Domain-Hosters (IONOS, Strato, …) oder Diensten wie Resend/Postmark im SMTP-Modus. `SMTP_FROM` sollte zur `SMTP_USER`-Domain passen, sonst landen Mails leicht im Spam.
|
||||
Ob der Versand funktioniert, zeigt **Admin → E-Mail-Test**: Token, Anmeldung, Test-Mail an die eigene Admin-Adresse und bei einem Fehler die Meldung von Microsoft samt Ursache.
|
||||
|
||||
Ob der Versand funktioniert, zeigt **Admin → E-Mail-Test**: Anmeldung, Test-Mail an die eigene Admin-Adresse und bei einem Fehler die Meldung des Mailservers samt Ursache.
|
||||
|
||||
### Microsoft 365 mit OAuth 2.0 (empfohlen)
|
||||
|
||||
Statt eines Benutzerpassworts meldet sich der Shop als eigene App-Registrierung an (Client-Credentials-Flow, SMTP mit XOAUTH2). Das funktioniert mit aktivierten Sicherheitsstandards und MFA, übersteht das Ende der Passwort-Anmeldung (Basic Auth) bei Exchange Online und gibt der App Zugriff auf genau ein Postfach. Aktiv, sobald `SMTP_OAUTH_TENANT_ID`, `SMTP_OAUTH_CLIENT_ID` und `SMTP_OAUTH_CLIENT_SECRET` gesetzt sind; `SMTP_PASSWORD` wird dann nicht gebraucht, `SMTP_HOST` ist standardmäßig `smtp.office365.com`.
|
||||
**Einrichtung** (einmalig; die SSO-App-Registrierung aus `.env.example` muss schon bestehen):
|
||||
|
||||
1. **Absender-Postfach:** Am besten ein freigegebenes Postfach (z. B. `[email protected]`) — braucht keine Lizenz und hat kein Passwort, mit dem sich jemand anmelden könnte. `SMTP_USER` und die Adresse in `SMTP_FROM` sind dieses Postfach.
|
||||
2. **App registrieren:** Entra Admin Center → App-Registrierungen → Neue Registrierung, z. B. „Wabenhain Shop Mail“, nur dieses Verzeichnis, keine Umleitungs-URI. Aus der Übersicht notieren: **Anwendungs-ID (Client-ID)** und **Verzeichnis-ID (Mandanten-ID)**.
|
||||
3. **Berechtigung:** API-Berechtigungen → Berechtigung hinzufügen → „Von meiner Organisation verwendete APIs“ → **Office 365 Exchange Online** → Anwendungsberechtigungen → **SMTP.SendAsApp** → danach „Administratorzustimmung erteilen“.
|
||||
4. **Geheimer Schlüssel:** Zertifikate & Geheimnisse → Neuer geheimer Clientschlüssel → den **Wert** kopieren (nicht die Geheimnis-ID; der Wert wird nur einmal angezeigt). Ablaufdatum im Kalender eintragen.
|
||||
5. **Objekt-ID der Unternehmensanwendung:** Entra → Unternehmensanwendungen → die App → **Objekt-ID** (nicht die der App-Registrierung).
|
||||
6. **Exchange-Zugriff auf genau dieses Postfach** (Exchange Online PowerShell):
|
||||
2. **Berechtigung an der SSO-App:** Entra Admin Center → App-Registrierungen → die SSO-App → API-Berechtigungen → Berechtigung hinzufügen → „Von meiner Organisation verwendete APIs“ → **Office 365 Exchange Online** → Anwendungsberechtigungen → **SMTP.SendAsApp** → danach „Administratorzustimmung erteilen“.
|
||||
3. **Objekt-ID der Unternehmensanwendung:** Entra → Unternehmensanwendungen → die SSO-App → **Objekt-ID** (nicht die der App-Registrierung).
|
||||
4. **Exchange-Zugriff auf genau dieses Postfach** (Exchange Online PowerShell):
|
||||
```powershell
|
||||
Install-Module ExchangeOnlineManagement # einmalig
|
||||
Connect-ExchangeOnline
|
||||
New-ServicePrincipal -AppId <Client-ID> -ObjectId <Objekt-ID aus Schritt 5> -DisplayName "Wabenhain Shop Mail"
|
||||
Add-MailboxPermission -Identity "[email protected]" -User <Objekt-ID aus Schritt 5> -AccessRights FullAccess
|
||||
New-ServicePrincipal -AppId <ENTRA_CLIENT_ID> -ObjectId <Objekt-ID aus Schritt 3> -DisplayName "Wabenhain Shop"
|
||||
Add-MailboxPermission -Identity "[email protected]" -User <Objekt-ID aus Schritt 3> -AccessRights FullAccess
|
||||
Set-CASMailbox -Identity "[email protected]" -SmtpClientAuthenticationDisabled $false
|
||||
```
|
||||
Die letzte Zeile ist nötig, weil auch OAuth über „authentifiziertes SMTP“ läuft. Änderungen brauchen bis zu 30 Minuten.
|
||||
7. **Kubernetes:** `SMTP_OAUTH_TENANT_ID` und `SMTP_OAUTH_CLIENT_ID` in `k8s/01-configmap.yaml`, `SMTP_OAUTH_CLIENT_SECRET` und `SMTP_USER` im Secret, `SMTP_PASSWORD` leer. Danach `kubectl rollout restart deploy/wabenhain-app -n wabenhain` und **Admin → E-Mail-Test**.
|
||||
8. **Aufräumen:** Ein zuvor für SMTP genutztes Benutzerpasswort ändern und aus dem Secret entfernen.
|
||||
5. **Kubernetes:** `SMTP_USER` im Secret setzen; `SMTP_HOST`, `SMTP_PORT` und `SMTP_PASSWORD` aus bestehenden Secrets/ConfigMaps entfernen. Danach `kubectl rollout restart deploy/wabenhain-app -n wabenhain` und **Admin → E-Mail-Test**.
|
||||
6. **Aufräumen:** Das bisher für SMTP genutzte Benutzerpasswort ändern; „Authentifiziertes SMTP“ bleibt nur für das Absender-Postfach an.
|
||||
|
||||
Läuft der Schlüssel ab, meldet der E-Mail-Test „Das Client-Secret ist abgelaufen“ (AADSTS7000222): neuen Schlüssel anlegen, Secret ersetzen, Pod neu starten.
|
||||
**Sicherheit:** Das SSO-Client-Secret darf damit auch senden — Exchange beschränkt das aber auf das eine freigegebene Postfach. Läuft das Secret ab, fallen **Anmeldung und Mailversand gleichzeitig** aus; der E-Mail-Test meldet dann „Das Client-Secret ist abgelaufen“ (AADSTS7000222): neuen Schlüssel anlegen, `ENTRA_CLIENT_SECRET` ersetzen, Pod neu starten. Ablaufdatum im Kalender eintragen.
|
||||
|
||||
## CAPTCHA (Cloudflare Turnstile)
|
||||
|
||||
@@ -363,7 +357,7 @@ Die App läuft danach hinter [Caddy](https://caddyserver.com/) unter `http://loc
|
||||
pnpm preflight
|
||||
```
|
||||
|
||||
**Geprüft wird die Umgebung, in der der Befehl läuft — nicht „das Projekt".** Auf einem Entwicklungsrechner liest er die lokale `.env`, und die enthält absichtlich einen `sk_test`-Schlüssel, kein SMTP und keine echte Domain. Er meldet dort also Blocker, die für den Live-Betrieb längst erledigt sind: `NEXT_PUBLIC_SITE_URL`, `SMTP_FROM` und die Steuersätze stehen in `k8s/01-configmap.yaml`, die Geheimnisse in `k8s/02-secret.yaml`. Wer das ein paarmal erlebt, gewöhnt sich an, über den Bericht hinwegzulesen — und übersieht dann den einen Blocker, der echt ist.
|
||||
**Geprüft wird die Umgebung, in der der Befehl läuft — nicht „das Projekt".** Auf einem Entwicklungsrechner liest er die lokale `.env`, und die enthält absichtlich einen `sk_test`-Schlüssel, keinen Mailversand und keine echte Domain. Er meldet dort also Blocker, die für den Live-Betrieb längst erledigt sind: `NEXT_PUBLIC_SITE_URL`, `SMTP_FROM` und die Steuersätze stehen in `k8s/01-configmap.yaml`, die Geheimnisse in `k8s/02-secret.yaml`. Wer das ein paarmal erlebt, gewöhnt sich an, über den Bericht hinwegzulesen — und übersieht dann den einen Blocker, der echt ist.
|
||||
|
||||
Für eine belastbare Aussage deshalb **im laufenden Pod** ausführen, wo ConfigMap und Secret tatsächlich anliegen:
|
||||
|
||||
@@ -378,7 +372,7 @@ Prüft die Konfiguration gegen alles, was einen öffentlichen Shop still unbrauc
|
||||
- `SESSION_SECRET`, `ADMIN_EMAIL` und `ADMIN_PASSWORD` auf Länge und darauf, dass noch der Beispielwert aus `.env.example` darin steht (ein bekanntes Signiergeheimnis macht jede Sitzung fälschbar).
|
||||
- `NEXT_PUBLIC_SITE_URL` auf HTTPS und darauf, dass dort nicht `localhost` steht — genau dieser Wert landet in Bestellbestätigungen, in `sitemap.xml` und in allen Canonical-Tags.
|
||||
- Stripe: Schlüssel und Webhook-Secret vorhanden, und ob versehentlich noch ein `sk_test`-Schlüssel gesetzt ist. Ohne Webhook-Secret wird keine Zahlung bestätigt: Bestellungen bleiben auf „NEU" stehen, ohne Rechnung und ohne Bestätigungsmail.
|
||||
- SMTP vollständig gesetzt — fehlt es, werden E-Mails nur ins Server-Log geschrieben statt versendet (siehe `lib/mail.ts`), also keine Bestellbestätigung und keine Passwort-Zurücksetzung.
|
||||
- Mailversand eingerichtet (`SMTP_USER` + `ENTRA_*`) — fehlt es, werden E-Mails nur ins Server-Log geschrieben statt versendet (siehe `lib/mail.ts`), also keine Bestellbestätigung und keine Passwort-Zurücksetzung.
|
||||
- Rechtstexte (Impressum, Datenschutz, AGB, Widerrufsbelehrung) und Telefonnummer auf verbliebene `[PLATZHALTER]`/`[PLACEHOLDER]`-Markierungen — je Sprachfassung einzeln benannt. Ein unvollständiges Impressum nach § 5 TMG ist in Deutschland abmahnfähig.
|
||||
|
||||
Diese Prüfung fragt die **Datenbank**, nicht den Quelltext: all diese Texte sind im Admin-Bereich unter „Inhalte" pflegbar (siehe `lib/content-registry.ts`), die Fassungen in `src/i18n/dictionaries/` sind nur der Rückfallwert, solange dort nichts gepflegt ist. Zum Ausfüllen des Impressums ist also **kein Deploy nötig** — und eine Prüfung, die nur in den Quelltext schaut, würde einen im Admin längst geschlossenen Blocker weiter melden (und umgekehrt schweigen, wenn jemand einen Platzhaltertext erst im Admin einträgt).
|
||||
|
||||
Reference in new issue
Block a user