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