APICORE_API_KEY replaces the service-account login: the key is sent as
the bearer token directly, needs no password and no MFA exemption, may
only read the shop feed and can be revoked in ApiCore at any time. A
malformed key is rejected with a clear message before any request.
Email/password stays as fallback for ApiCore versions without keys.
The system status treats the key or the legacy login as configured.
README, .env.example and the k8s secret template describe the key.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
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]>
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]>
/bestellung-verfolgen holt den Sendungsverlauf serverseitig ueber die
DHL Shipment Tracking - Unified API und zeigt ihn im eigenen Layout an,
statt Kundschaft auf dhl.de weiterzuleiten.
- lib/dhl-tracking.ts: Abfrage mit API-Key, Zod-Validierung, Timeout,
Cache pro Sendung (unterwegs 10 min, zugestellt 12 h, Fehler 2 min)
- Abfrage erst nach erfolgreicher Pruefung von Bestellnummer + E-Mail,
damit die Seite nicht als offener Proxy zu DHL dient
- Ohne freigeschalteten Key bleibt die Seite unveraendert (Nummer + Link)
- Optional DHL_TRACKING_API_KEY, falls das Tracking in eigener DHL-App liegt
- Datenschutzerklaerung um die Statusabfrage bei DHL ergaenzt
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
01-configmap.yaml und 02-secret.example.yaml sind jetzt so geschrieben, dass
sie sich als Produktivkonfiguration uebernehmen lassen: in drei Abschnitte
gegliedert (ANPASSEN / Vorgaben / optional), jeder Wert mit der Angabe, was
ohne ihn nicht funktioniert. Anzupassen sind genau zwei Werte in der
ConfigMap und die mit "CHANGE-ME" markierten im Secret; alles Uebrige traegt
eine brauchbare Vorgabe oder darf leer bleiben.
Beide Dateien decken zusammen jede Umgebungsvariable ab, die die Anwendung
liest, und jeder Schluessel steht in genau einer der beiden. Bisher war das
Handarbeit und driftete: GOOGLE_PLACES_API_KEY lag in der unverschluesselten
ConfigMap, APICORE_API_URL unnoetig im Secret.
Neu ist deshalb src/lib/k8s-manifest-env.test.ts. Der Test liest die
process.env-Zugriffe unter src/ und die Schluessel der beiden Manifeste und
prueft vier Zusagen: keine gelesene Variable fehlt, kein Schluessel steht
doppelt (sonst entschiede allein die Reihenfolge der envFrom-Eintraege,
welcher Wert gewinnt), kein Schluessel ist tot, und die committete
Secret-Vorlage enthaelt keine ausgefuellten Werte.
Beim ersten Lauf hat der Test zwei Dinge gefunden. ADMIN_EMAIL und
ADMIN_PASSWORD liest nicht die App, sondern prisma/seed.ts — das ist
legitim und steht jetzt begruendet in einer Ausnahmeliste, zusammen mit
DB_PASSWORD fuer das Datenbank-StatefulSet. STRIPE_PUBLISHABLE_KEY dagegen
wird von keiner einzigen Code-Stelle gelesen: der Checkout laeuft
vollstaendig serverseitig ueber eine Stripe-Checkout-Session. Der Schluessel
ist aus Secret-Vorlage, .env.example, docker-compose.yml und README
entfernt, mit Hinweis, wofuer man ihn braeuchte.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Die Manifeste in k8s/ trugen noch Platzhalter, waehrend im Cluster laengst
die echten Werte stehen. Wer sie unveraendert anwendet, ueberschreibt damit
funktionierende Einstellungen.
- Domain: shop.example.com -> wabenhain.com, in 06-ingress.yaml (host und
tls.hosts) und in 01-configmap.yaml (NEXT_PUBLIC_SITE_URL). Beide Stellen
muessen uebereinstimmen; aus NEXT_PUBLIC_SITE_URL entstehen Sitemap,
Canonical-Tags und jeder Link in jeder Bestellbestaetigung.
- GOOGLE_PLACES_API_KEY von der ConfigMap ins Secret. Das ist ein
abrechenbarer Zugangsschluessel, und eine ConfigMap ist unverschluesselt
und in der Cluster-Oberflaeche im Klartext lesbar. Die zugehoerige
GOOGLE_PLACE_ID ist oeffentlich und bleibt in der ConfigMap. Der Cluster
handhabt das bereits so.
- APICORE_API_URL vom Secret in die ConfigMap: eine Adresse ist kein
Geheimnis. Auch das entspricht dem Cluster.
- VIDEO_TRANSCODE_THREADS in die ConfigMap aufgenommen, damit die
Threadbegrenzung des Video-Transcodes (siehe vorheriger Commit) auch ueber
das Deployment einstellbar ist.
Gegenprobe: die Vereinigung aller Schluessel aus ConfigMap und Secret deckt
jetzt genau die Umgebungsvariablen ab, die der Code ueber process.env liest
- keine fehlt. Die beiden Ueberhaenge sind beabsichtigt: DB_PASSWORD liest
der Postgres-StatefulSet, STRIPE_PUBLISHABLE_KEY haelt die Konfiguration
vollstaendig fuer eine kuenftige clientseitige Stripe-Einbindung.
Der Hinweistext der Go-Live-Pruefung nannte shop.example.com als
ausgelieferte Vorgabe; das stimmt nicht mehr. Die Pruefung selbst ist
unveraendert.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Dockerfile (Multi-Stage, "output: standalone"), docker-compose.yml mit
Caddy als einzigem nach aussen offenem Reverse-Proxy (verhindert
X-Forwarded-For-Faelschung fuer das App-eigene Rate-Limiting), Gitea
Actions Workflow der bei jedem Push zwei Image-Tags in die Gitea-
Container-Registry baut (schlankes Laufzeit-Image + ein Migrations-
Image mit Prisma-CLI), sowie rohe Kubernetes-Manifeste (Namespace,
ConfigMap, Secret-Vorlage, Postgres-/MySQL-StatefulSet, Migrations-Job,
App-Deployment, Ingress) fuer den Cluster-Betrieb. Siehe k8s/README.md
fuer den vollstaendigen Einrichtungs-/Deployment-Ablauf.