From de82ad0233103c575b71d2a9d5aee1d015062085 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Sun, 27 Sep 2026 00:07:34 +0200 Subject: [PATCH] feat(admin): settings page with tabs, system checks and email test "Inhalte" becomes Admin -> Einstellungen, grouped into tabs so each area stands on its own: Firma & Versand, Marke, Startseite, Seiten, Social Media, Rechtstexte and System & Tests. Tabs are plain links (?reiter=...), bookmarkable and usable without JavaScript; saving returns to the same tab. System & Tests brings everything that checks the running shop into one place: - integration overview (Stripe, Stripe webhook, Microsoft 365 mail, Microsoft sign-in, Turnstile, Google reviews, DHL, ApiCore): only whether each is set up and the names of missing variables, never a value; warns when Stripe runs on test keys or mixes test/live - the email test, now with the IPv4 network checks for smtp.office365.com:587 and login.microsoftonline.com:443 /admin/inhalte and /admin/e-mail-test redirect to the new page; the separate "E-Mail-Test" menu entry is gone. The tab parameter and the saved group are validated against a fixed list before they are used in a redirect. Docs point to the new location and cover the ENETUNREACH (IPv6) case. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 4 +- docs/EINRICHTUNG.md | 11 +- src/app/admin/actions/content.ts | 7 +- src/app/admin/e-mail-test/page.tsx | 20 +- src/app/admin/einstellungen/content-form.tsx | 199 ++++++++++++++ src/app/admin/einstellungen/page.tsx | 142 ++++++++++ .../smtp-test-actions.ts} | 0 .../smtp-test-panel.tsx | 21 +- src/app/admin/inhalte/page.tsx | 258 +----------------- src/components/admin-shell.tsx | 3 +- src/i18n/dictionaries/de.ts | 39 ++- src/i18n/dictionaries/en.ts | 39 ++- src/i18n/dictionary-type.ts | 26 +- src/lib/integration-status.test.ts | 57 ++++ src/lib/integration-status.ts | 55 ++++ src/lib/settings-tabs.test.ts | 25 ++ src/lib/settings-tabs.ts | 36 +++ 17 files changed, 654 insertions(+), 288 deletions(-) create mode 100644 src/app/admin/einstellungen/content-form.tsx create mode 100644 src/app/admin/einstellungen/page.tsx rename src/app/admin/{e-mail-test/actions.ts => einstellungen/smtp-test-actions.ts} (100%) rename src/app/admin/{e-mail-test => einstellungen}/smtp-test-panel.tsx (70%) create mode 100644 src/lib/integration-status.test.ts create mode 100644 src/lib/integration-status.ts create mode 100644 src/lib/settings-tabs.test.ts create mode 100644 src/lib/settings-tabs.ts diff --git a/README.md b/README.md index 0d18e70..9bbb26b 100644 --- a/README.md +++ b/README.md @@ -262,7 +262,7 @@ Die Antwort wird eine Stunde zwischengespeichert (`revalidate: 3600`), was die K 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. -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 → Einstellungen → System & Tests**: Token, Anmeldung, Test-Mail an die eigene Admin-Adresse und bei einem Fehler die Meldung von Microsoft samt Ursache. **Einrichtung** (einmalig; die SSO-App-Registrierung aus `.env.example` muss schon bestehen): @@ -278,7 +278,7 @@ Ob der Versand funktioniert, zeigt **Admin → E-Mail-Test**: Token, Anmeldung, Set-CASMailbox -Identity "hallo@wabenhain.com" -SmtpClientAuthenticationDisabled $false ``` Die letzte Zeile ist nötig, weil auch OAuth über „authentifiziertes SMTP“ läuft. Änderungen brauchen bis zu 30 Minuten. -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**. +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 → Einstellungen → System & Tests**. 6. **Aufräumen:** Das bisher für SMTP genutzte Benutzerpasswort ändern; „Authentifiziertes SMTP“ bleibt nur für das Absender-Postfach an. **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. diff --git a/docs/EINRICHTUNG.md b/docs/EINRICHTUNG.md index b8a5db4..d64e00a 100644 --- a/docs/EINRICHTUNG.md +++ b/docs/EINRICHTUNG.md @@ -377,9 +377,9 @@ Der Reihe nach: Jeder fehlende Pflichtwert erscheint als Blocker mit Begründung. 2. **Shop erreichbar:** `https://wabenhain.com` öffnen. 3. **Admin-Login:** `https://wabenhain.com/konto` mit `ADMIN_EMAIL`/`ADMIN_PASSWORD` anmelden → Zwei-Faktor-Authentifizierung einrichten → Admin-Bereich öffnet sich. -4. **Mailversand:** **Admin → E-Mail-Test** → „Test-Mail senden“. Erfolg: grüne Meldung und eine Mail an die Admin-Adresse. Fehler: die Seite zeigt die Meldung von Microsoft und was zu tun ist (siehe [Fehlerbehebung](#11-fehlerbehebung)). +4. **Mailversand und Anbindungen:** **Admin → Einstellungen → System & Tests**. Oben steht, welche Dienste eingerichtet sind (fehlende Werte werden benannt). Darunter „Test-Mail senden“: zuerst prüft der Test, ob `smtp.office365.com:587` und `login.microsoftonline.com:443` per IPv4 erreichbar sind, dann den Versand. Erfolg: grüne Meldung und eine Mail an die Admin-Adresse. Fehler: die Seite zeigt die Meldung von Microsoft und was zu tun ist (siehe [Fehlerbehebung](#11-fehlerbehebung)). 5. **Microsoft-Anmeldung:** abmelden, auf `/konto` „Mit Microsoft anmelden“ wählen. -6. **Firmendaten:** **Admin → Inhalte → Firmendaten** — Name, Anschrift und eine **gültige** E-Mail-Adresse eintragen. Sie erscheinen im Impressum und in der Widerrufsbelehrung. +6. **Firmendaten:** **Admin → Einstellungen → Firma & Versand** — Name, Anschrift und eine **gültige** E-Mail-Adresse eintragen. Sie erscheinen im Impressum und in der Widerrufsbelehrung. 7. **Bezahlung:** eine kleine echte Bestellung aufgeben, bezahlen, im Admin prüfen: Status „Bezahlt“, Rechnung vorhanden, Bestätigungsmail angekommen. Danach im Admin erstatten. 8. **Widerruf:** im Footer „Vertrag widerrufen“ testen → Eingangsbestätigung kommt an, Eintrag erscheint unter **Admin → Widerrufe**. @@ -395,13 +395,13 @@ Der Reihe nach: | **DHL-/Google-/Turnstile-Schlüssel** | je nach Anbieter | Funktion ruht | Beim Anbieter erneuern, Secret ersetzen | | **Postfach-Freigabe** | bei Umbenennung oder neuem Absender-Postfach | Mailversand scheitert („Exchange lässt die App nicht …“) | Abschnitt 4.2 für das neue Postfach wiederholen, `SMTP_USER` und `SMTP_FROM` anpassen | -Nach **jeder** Änderung an Secret oder ConfigMap: `kubectl rollout restart deployment/wabenhain-app -n wabenhain` und kurz **Admin → E-Mail-Test**. +Nach **jeder** Änderung an Secret oder ConfigMap: `kubectl rollout restart deployment/wabenhain-app -n wabenhain` und kurz **Admin → Einstellungen → System & Tests**. --- ## 11. Fehlerbehebung -### Mailversand (Anzeige in **Admin → E-Mail-Test**) +### Mailversand (Anzeige in **Admin → Einstellungen → System & Tests**) | Meldung / Ursache | Lösung | |---|---| @@ -413,7 +413,8 @@ Nach **jeder** Änderung an Secret oder ConfigMap: `kubectl rollout restart depl | „Exchange lässt die App nicht mit dem Postfach senden“ (`535 5.7.3`) | (1) `SMTP.SendAsApp` erteilt? (3.3) (2) `New-ServicePrincipal` mit der Objekt-ID der **Unternehmensanwendung**? (3.4/4.2) (3) `Add-MailboxPermission` für genau `SMTP_USER`? (4) 30 Minuten gewartet? | | „Authentifiziertes SMTP ausgeschaltet“ (`SmtpClientAuthentication is disabled`) | `Set-CASMailbox -Identity "hallo@wabenhain.com" -SmtpClientAuthenticationDisabled $false` | | „SendAsDenied“ | `SMTP_FROM` ist nicht die Adresse aus `SMTP_USER` → angleichen | -| „nicht erreichbar“ (Netzwerk) | Cluster muss ausgehend `smtp.office365.com:587` und `login.microsoftonline.com:443` erreichen (Firewall, NetworkPolicy) | +| „nicht erreichbar“ (Netzwerk) | Die Netzwerkprüfung im Test zeigt, welcher Weg fehlt. Der Cluster muss ausgehend per IPv4 `smtp.office365.com:587` und `login.microsoftonline.com:443` erreichen (Firewall des Hosters, NetworkPolicy). Manche Hoster sperren Mail-Ports für neue Server — dort die Freigabe von Port 587 beantragen. | +| `ENETUNREACH 2603:…` (IPv6-Adresse) | Stammte aus älteren Versionen, die zufällig eine IPv6-Adresse wählten; der Shop verbindet sich inzwischen nur per IPv4. Tritt es trotzdem auf: aktuelles Image ausrollen. | Ausführliche Fehler stehen außerdem im Pod-Log: diff --git a/src/app/admin/actions/content.ts b/src/app/admin/actions/content.ts index 7e251b8..f576885 100644 --- a/src/app/admin/actions/content.ts +++ b/src/app/admin/actions/content.ts @@ -8,6 +8,7 @@ import { isValidUploadPath, isValidVideoUploadPath } from "@/lib/uploads"; import { requireAdmin } from "./_shared"; import { fail, succeed } from "@/lib/flash"; import { normalizeContactEmail } from "@/lib/contact-email"; +import { settingsPathForGroup } from "@/lib/settings-tabs"; const COMPANY_EMAIL_KEY = "company.email"; @@ -16,7 +17,7 @@ const COMPANY_EMAIL_KEY = "company.email"; * — jede Gruppen-Form sendet nur ihre eigenen Feldnamen, diese Action * verarbeitet einfach, was tatsaechlich in formData vorhanden ist. Nicht * lokalisierte Felder kommen als "{key}", lokalisierte als "de::{key}" / - * "en::{key}" (siehe admin/inhalte/page.tsx). + * "en::{key}" (siehe admin/einstellungen/content-form.tsx). */ export async function updateSiteContentAction(formData: FormData): Promise { const admin = await requireAdmin(); @@ -59,7 +60,7 @@ export async function updateSiteContentAction(formData: FormData): Promise // Impressum ein Kontaktweg, der ins Leere fuehrt. if (def.key === COMPANY_EMAIL_KEY && normalizeContactEmail(valueDe) === null) { return fail( - "/admin/inhalte", + settingsPathForGroup(group), "Die Firmen-E-Mail ergibt keine gültige Adresse. Möglich sind z. B. info@wabenhain.com, info [at] wabenhain.com oder info [á] wabenhain.com." ); } @@ -85,5 +86,5 @@ export async function updateSiteContentAction(formData: FormData): Promise }); revalidatePath("/", "layout"); - return succeed("/admin/inhalte", "Inhalte gespeichert."); + return succeed(settingsPathForGroup(group), "Einstellungen gespeichert."); } diff --git a/src/app/admin/e-mail-test/page.tsx b/src/app/admin/e-mail-test/page.tsx index f795629..2f530f8 100644 --- a/src/app/admin/e-mail-test/page.tsx +++ b/src/app/admin/e-mail-test/page.tsx @@ -1,17 +1,7 @@ -import { BackLink } from "@/components/back-link"; -import { AdminPageHeader } from "@/components/admin-list"; -import { getLocale } from "@/i18n/locale"; -import { getDictionary } from "@/i18n/get-dictionary"; -import { SmtpTestPanel } from "./smtp-test-panel"; +import { redirect } from "next/navigation"; +import { SETTINGS_PATH } from "@/lib/settings-tabs"; -export default async function AdminEmailTestPage() { - const t = getDictionary(await getLocale()); - return ( -
- - -

{t.adminEmailTestPage.intro}

- -
- ); +/** Der E-Mail-Test liegt jetzt in Admin → Einstellungen → System & Tests. */ +export default function AdminEmailTestRedirect(): never { + redirect(`${SETTINGS_PATH}?reiter=system`); } diff --git a/src/app/admin/einstellungen/content-form.tsx b/src/app/admin/einstellungen/content-form.tsx new file mode 100644 index 0000000..f65a873 --- /dev/null +++ b/src/app/admin/einstellungen/content-form.tsx @@ -0,0 +1,199 @@ +import { prisma } from "@/lib/prisma"; +import { AdminImageField } from "@/components/admin-image-field"; +import { AdminVideoField } from "@/components/admin-video-field"; +import { CONTENT_REGISTRY, type ContentFieldDef } from "@/lib/content-registry"; +import { updateSiteContentAction } from "../actions"; +import type { Dictionary } from "@/i18n/dictionary-type"; +import type { Locale } from "@/i18n/config"; +import { Button } from "@/components/ui"; + +const ROWS_BY_TYPE: Record = { + text: 1, + textarea: 3, + // Auswahlfelder rendern ein