Sending no longer needs a user password for Microsoft 365. When SMTP_OAUTH_TENANT_ID, SMTP_OAUTH_CLIENT_ID and SMTP_OAUTH_CLIENT_SECRET are set, the shop fetches an app-only token from Entra ID and signs in to smtp.office365.com with XOAUTH2. This works with security defaults and MFA, survives the end of SMTP basic auth (default off from end of December 2026) and limits the app to the one mailbox Exchange grants it (SMTP.SendAsApp + Add-MailboxPermission). Password login stays available for other providers; a partial OAuth setup never silently falls back. - lib/ms365-oauth.ts: token cached until 5 minutes before expiry, concurrent requests share one fetch, forced refresh after a server rejection, 15 s timeout, tenant id validated before it enters the URL, client secret redacted from every error. - lib/smtp-diagnose.ts: explains AADSTS7000215 (secret ID instead of value), 7000222 (expired), 700016 (unknown app), 90002/900023 (tenant) and maps a rejected XOAUTH2 login to missing mailbox access. - Admin e-mail test shows the auth mode and lists missing settings. - k8s manifests, preflight check and README (step-by-step Entra and Exchange setup) updated; misplaced SMTP_PORT comment moved. Verified against Entra ID and smtp.office365.com with fake values: AADSTS90002 is classified as tenant error without leaking the secret, an invalid token yields 535 5.7.3 and is classified as mailbox access. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
148 lines
7.2 KiB
YAML
148 lines
7.2 KiB
YAML
# Nicht-geheime Konfiguration. Geheimnisse (Passwoerter, API-Schluessel)
|
|
# gehoeren in 02-secret.yaml, NIE hierher: eine ConfigMap ist unverschluesselt
|
|
# und in der Cluster-Oberflaeche im Klartext lesbar.
|
|
#
|
|
# ANPASSEN vor dem ersten Ausrollen: NEXT_PUBLIC_SITE_URL und SMTP_FROM. Alle
|
|
# uebrigen Werte sind sinnvolle Vorgaben und koennen so bleiben; die leeren
|
|
# ("") gehoeren zu optionalen Funktionen, die dann einfach ruhen.
|
|
#
|
|
# Zusammen mit 02-secret.yaml deckt diese Datei JEDE Umgebungsvariable ab, die
|
|
# die Anwendung liest. Die Vollstaendigkeit wird automatisch geprueft, siehe
|
|
# src/lib/k8s-manifest-env.test.ts — kommt im Code eine neue Variable dazu,
|
|
# faellt das beim naechsten "pnpm test" auf, nicht erst im Betrieb.
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: wabenhain-config
|
|
namespace: wabenhain
|
|
data:
|
|
# ---------------------------------------------------------------------
|
|
# ANPASSEN
|
|
# ---------------------------------------------------------------------
|
|
|
|
# Oeffentliche Adresse des Shops. Einzige Quelle fuer Sitemap,
|
|
# Canonical-Tags, JSON-LD und JEDEN Link in JEDER E-Mail (src/lib/url.ts).
|
|
# Muss zum "host" in 06-ingress.yaml passen — mit https, ohne
|
|
# abschliessenden Schraegstrich.
|
|
NEXT_PUBLIC_SITE_URL: "https://wabenhain.com"
|
|
|
|
# Absenderadresse aller Transaktions-E-Mails. Sollte auf einer eigenen
|
|
# Domain liegen, sonst landen die Mails regelmaessig im Spam-Ordner.
|
|
SMTP_FROM: "Wabenhain <[email protected]>"
|
|
|
|
# Microsoft 365 per OAuth 2.0 (empfohlen, siehe README "E-Mail-Versand").
|
|
# Beide Werte sind nicht geheim und stehen im Entra Admin Center unter
|
|
# App-Registrierungen → <App> → Uebersicht. Zusammen mit
|
|
# SMTP_OAUTH_CLIENT_SECRET im Secret schaltet das den Versand auf OAuth um
|
|
# (SMTP_PASSWORD wird dann nicht gebraucht, SMTP_HOST ist optional).
|
|
# Leer lassen fuer Anmeldung per Passwort bei einem anderen Anbieter.
|
|
# Verzeichnis-ID (Mandanten-ID) oder z. B. wabenhain.onmicrosoft.com:
|
|
SMTP_OAUTH_TENANT_ID: ""
|
|
# Anwendungs-ID (Client-ID) der App-Registrierung:
|
|
SMTP_OAUTH_CLIENT_ID: ""
|
|
|
|
# ---------------------------------------------------------------------
|
|
# VORGABEN — koennen so bleiben
|
|
# ---------------------------------------------------------------------
|
|
|
|
NODE_ENV: "production"
|
|
|
|
# "postgresql" oder "mysql" — muss zu dem Datenbank-Manifest passen, das du
|
|
# tatsaechlich ausrollst (03-postgres.yaml bzw. 03-mysql.yaml), UND zum
|
|
# Schema in DATABASE_URL im Secret. Ein einzelnes Image traegt beide
|
|
# Prisma-Client-Varianten, siehe src/lib/prisma.ts.
|
|
DATABASE_PROVIDER: "postgresql"
|
|
|
|
# Gleichzeitig offene Datenbankverbindungen JE INSTANZ. Leer = 20.
|
|
#
|
|
# Beim Hochskalieren mitdenken: der Wert multipliziert sich mit "replicas"
|
|
# in 05-app-deployment.yaml. Drei Instanzen mit je 20 belegen 60 der
|
|
# voreingestellt 100 Verbindungen von PostgreSQL — daneben brauchen der
|
|
# Aufraeum-CronJob, die Sicherung und gelegentlich eine Konsole noch
|
|
# welche. Entweder diesen Wert senken oder max_connections anheben.
|
|
DATABASE_POOL_SIZE: "20"
|
|
|
|
# Port des SMTP-Anbieters. 587 ist STARTTLS (der Normalfall, auch fuer
|
|
# Microsoft 365), 465 waere implizites TLS.
|
|
SMTP_PORT: "587"
|
|
|
|
# Verzeichnis fuer hochgeladene Bilder und Videos. MUSS exakt dem mountPath
|
|
# des "uploads"-Volumes in 05-app-deployment.yaml entsprechen — die App
|
|
# weiss nicht, was dort gemountet ist, sie schreibt nur in diesen Pfad.
|
|
# Fehlt der Wert, schreibt die App nach "./uploads" relativ zum
|
|
# Arbeitsverzeichnis: das trifft im Image zufaellig denselben Ort, ist aber
|
|
# nirgends zugesichert. Deshalb hier ausdruecklich setzen.
|
|
UPLOAD_DIR: "/app/uploads"
|
|
|
|
# Ablage der Rechnungs-PDFs, nach Jahr und Kunde sortiert:
|
|
# /app/invoices/2026/mustermann-erika/RE-2026-A3F9C1B2.pdf
|
|
# Muss zum mountPath des "invoices"-Volumes in 05-app-deployment.yaml
|
|
# passen. BEWUSST ein anderes Verzeichnis als UPLOAD_DIR: auf die Uploads
|
|
# zeigt eine oeffentliche Route, auf dieses Verzeichnis keine. Siehe
|
|
# src/lib/invoice-storage.ts und 04-uploads-pvc.yaml.
|
|
INVOICE_DIR: "/app/invoices"
|
|
|
|
# Umsatzsteuersatz in GANZEN Prozent, der auf der Rechnung ausgewiesen wird.
|
|
# 7 = ermaessigter Satz (Honig als Lebensmittel, Anlage 2 zum UStG)
|
|
# 19 = voller Satz
|
|
# 0 = Kleinunternehmerregelung; die Rechnung traegt dann statt der
|
|
# Steuerzeilen den Pflichthinweis nach § 19 UStG
|
|
#
|
|
# Die Shop-Preise sind BRUTTOPREISE (§ 1 PAngV). Die Steuer wird daraus
|
|
# herausgerechnet, nicht aufgeschlagen — der Endpreis bleibt also genau der,
|
|
# den die Kundschaft im Warenkorb gesehen hat.
|
|
#
|
|
# Der Satz wird bei jeder Rechnung FESTGESCHRIEBEN. Eine Aenderung hier gilt
|
|
# nur fuer kuenftige Rechnungen; bereits ausgestellte behalten ihren Satz.
|
|
VAT_RATE_PERCENT: "7"
|
|
|
|
# Satz je Kategorie. Der Steuersatz haengt an der Art der Ware: Honig ist
|
|
# ein Lebensmittel und ermaessigt besteuert, eine Bienenwachskerze nicht.
|
|
# Fehlt hier ein Wert, gilt VAT_RATE_PERCENT von oben.
|
|
#
|
|
# PROPOLIS bitte pruefen: als Lebensmittel waeren 7% richtig, als
|
|
# Nahrungsergaenzungs- oder Kosmetikprodukt 19%. Voreingestellt ist der
|
|
# volle Satz, weil er im Zweifel der sicherere ist.
|
|
VAT_RATE_PERCENT_HONIG: "7"
|
|
VAT_RATE_PERCENT_KERZEN: "19"
|
|
VAT_RATE_PERCENT_PROPOLIS: "19"
|
|
|
|
# Threads fuer das Umrechnen hochgeladener Videos (Admin -> Inhalte ->
|
|
# Hero-Video). NICHT ohne Not erhoehen: ffmpeg teilt sich das Speicherlimit
|
|
# dieses Pods mit dem Node-Prozess, und jeder zusaetzliche Thread haelt
|
|
# eigene Bildpuffer — bei 4K-Material rund 12 MB je Bild. Zu viele Threads
|
|
# bringen den Pod beim Video-Upload um. Siehe src/lib/video-transcode.ts.
|
|
VIDEO_TRANSCODE_THREADS: "2"
|
|
|
|
# "sandbox", bis die DHL-Versandmarken getestet sind, danach "production".
|
|
# Wirkt nur, wenn die DHL-Zugangsdaten im Secret gesetzt sind.
|
|
DHL_ENVIRONMENT: "sandbox"
|
|
|
|
# Oeffentlicher Stripe-Schluessel (pk_live_...) fuer das eingebettete
|
|
# Zahlungsformular. Nicht geheim — er steht ohnehin im HTML der Kasse.
|
|
# Muss zum STRIPE_SECRET_KEY im Secret passen (beide live). Leer = der
|
|
# Checkout nimmt keine Bestellungen an.
|
|
STRIPE_PUBLISHABLE_KEY: ""
|
|
|
|
# ---------------------------------------------------------------------
|
|
# OPTIONAL — leer lassen, solange die Funktion nicht genutzt wird
|
|
# ---------------------------------------------------------------------
|
|
|
|
# Oeffentlicher Teil von Cloudflare Turnstile (CAPTCHA). Der geheime
|
|
# Gegenpart TURNSTILE_SECRET_KEY steht im Secret — beide setzen oder beide
|
|
# leer lassen. Leer = kein CAPTCHA-Widget, die Formulare bleiben nutzbar.
|
|
NEXT_PUBLIC_TURNSTILE_SITE_KEY: ""
|
|
|
|
# Google-Eintrag, aus dem die echten Rezensionen auf der Startseite kommen.
|
|
# Die Place-ID ist oeffentlich (sie steckt in jeder Google-Maps-Adresse des
|
|
# Eintrags) und darf deshalb hier stehen; der abrechenbare
|
|
# GOOGLE_PLACES_API_KEY liegt im Secret. Leer = statische Beispiel-Stimmen.
|
|
GOOGLE_PLACE_ID: ""
|
|
|
|
# Adresse der ApiCore-API fuer den Stammdaten-Sync (Ernten, Produkte,
|
|
# Chargen — siehe src/lib/apicore-client.ts), mit dem globalen Praefix
|
|
# "api", z.B. "https://apicore.example.org/api". Nur die Adresse; die
|
|
# Zugangsdaten des Sync-Kontos liegen im Secret. Leer = der Sync-Knopf im
|
|
# Admin meldet einen Konfigurationsfehler.
|
|
APICORE_API_URL: ""
|