Skip to main content

Dashboard-E-Mails (cockpitOS)

Was

Alle Dashboard-Benutzer-E-Mails laufen über Resend mit einheitlichem SaaS-Layout. Keine Passwörter im Klartext — nur Einmal-Links für Einrichtung und Reset. Center-Website-Formulare (Kontakt, Vermietung, ILG Promotionfläche) nutzen eigene Templates in email.ts mit Center-Kontext (Name, Formular-Typ, Website-URL).

Kunden-Postfächer und Weiterleitungen (info@kunde.de) sind ein anderes System — siehe Center-Mail. Formulare senden weiter über Resend an diese Adressen.

Warum

Professioneller SaaS-Standard: Setup- und Reset-Links statt Passwort per E-Mail (Risiko durch Postfach-Zugriff, Weiterleitung, Logs, Support-Screenshots).

Wer ist betroffen

GruppeInhalt
Redaktion / AdminsWillkommen, Einladung, Rollenänderung, Account-Status, Admin-Reset
Neue NutzerAccount einrichten über /auth/activate
EntwicklerTemplates, API-Hooks, Token-Logik
Betrieb / ITRESEND_API_KEY, NEXTAUTH_URL, Absender-Domain
Besuchernicht betroffen (Center-Mails separat)

Nutzer-Doku (Bedienung im Dashboard): Benutzer und Organisation.

Wo

Code

BereichPfad
Layout & Copyrightapps/dashboard/src/lib/email-layout.ts
Templates (15 Dashboard-Typen)apps/dashboard/src/lib/dashboard-email-templates.ts
Willkommen — rollenabhängige Bulletsapps/dashboard/src/lib/welcome-email-content.ts
Digest-Cron-Logikapps/dashboard/src/lib/email-digest.ts
Versand-Helferapps/dashboard/src/lib/email.ts
Token, URLs, Platzhalter-PWapps/dashboard/src/lib/user-account-tokens.ts
User-Onboarding-Flowsapps/dashboard/src/lib/user-account-email.ts (sendSelfServicePasswordResetEmail)
Auth-Footer (Copyright + Version)apps/dashboard/src/components/auth/cockpit-auth-footer.tsx, apps/dashboard/src/lib/cockpit-meta.ts, apps/dashboard/VERSION
UI Account einrichten / Resetapps/dashboard/src/components/auth/account-password-form.tsx
Testskriptscripts/test-dashboard-emails.tspnpm test:dashboard-emails

URLs (Dashboard-App)

PfadZweck
/auth/activate?token=…Neuer Account / Einladung — Passwort festlegen
/auth/reset-password?token=…Passwort vergessen oder Admin-Reset
/auth/unlock?token=…Account nach Login-Lockout entsperren
/auth/signin?message=account-activatedErfolg nach Einrichtung
/auth/signin?message=password-reset-successErfolg nach Reset
/mail-alias/verify?token=…Externe Center-Mail-Weiterleitung bestätigen (kein Login)

API (Auslöser)

EndpointE-Mail
POST /api/usersWillkommen oder Einladung (invite: true); Response enthält emailSent: boolean
POST /api/users/{userId}/reset-passwordAdmin-Reset-Link
PUT /api/users/{userId} (newPassword)Admin-Reset-Link (kein direktes Setzen mehr)
PUT /api/users/{userId} (Rolle / isActive)Rolle / Deaktiviert / Reaktiviert
DELETE /api/users/{userId}Account gelöscht
POST /api/auth/forgot-passwordPasswort vergessen (Self-Service; gleiche Token-/Resend-Pipeline wie Admin-Reset)
POST /api/auth/reset-passwordBestätigung Passwort geändert (nach Token-Reset)
POST /api/users/me/change-passwordBestätigung Passwort geändert (Self-Service)
Social-WorkflowSOCIAL_REVIEW_REQUEST (Freigabe-Anfrage an Prüfer) immer sofort; SOCIAL_REVIEW (abgelehnt / Publish-Fehler sofort; freigegeben nur sofort wenn Digest aus)
POST /api/cron/email-digestEMAIL_DIGEST (täglich, opt-in, nur bei Inhalt)
GET/PATCH /api/users/me/notification-preferencesDigest ein/aus + Versandstunde
POST /api/centers/{centerId}/mail-aliases (externes Ziel)MAIL_ALIAS_VERIFY an die Zieladresse
POST /api/centers/{centerId}/mail-aliases/{aliasId}/verifydieselbe Mail erneut

Token-Speicher: User.resetToken, User.resetTokenExpiry, User.accountTokenPurpose (setup | reset | unlock). Lockout: failedLoginAttempts, lockedUntil. Migration: packages/database/migrations/20260606120000_add_user_account_security_SAFE.sql (additiv).

Templates (vollständig)

IDBetreff-KontextLink / Gültigkeit
PASSWORD_RESETPasswort vergessen/auth/reset-password24 h
WELCOMEUser angelegt/auth/activate (purpose=setup) — 7 Tage; Bullets nach Rolle (Admin / Redaktion / CM-App / Read-only), optional CM-App-Hinweis
USER_INVITATIONEinladung (invite: true)/auth/activate (purpose=setup) — 7 Tage
PASSWORD_CHANGEDNach erfolgreichem Reset
ACCOUNT_LOCKEDNach 5 Fehlversuchen (Login)/auth/unlock (24 h)
ROLE_CHANGERolle geändert
PASSWORD_RESET_BY_ADMINAdmin-Reset/auth/reset-password24 h
ACCOUNT_DEACTIVATEDAccount deaktiviert
ACCOUNT_REACTIVATEDAccount reaktiviertLink zur Anmeldung
ACCOUNT_DELETEDAccount gelöscht
SOCIAL_REVIEW_REQUESTFreigabe angefordert (Approver)Link zum Social-Workflow
SOCIAL_REVIEWFreigabe-Ergebnis (Redakteur)Dashboard-URLs zum Post
CLIENT_REPORT_AVAILABLEMonatsbericht bereit (extern)Schreibgeschützter Share-Link
EMAIL_DIGESTTägliche Redaktions-ZusammenfassungEinstellungen + Dashboard
MAIL_ALIAS_VERIFYExterne Weiterleitung bestätigen (an das Ziel)/mail-alias/verify7 Tage

Willkommens-Mail nach Rolle

Ein Template (WELCOME), vier Zielgruppen über welcome-email-content.ts:

ZielgruppeRollen (Auszug)Inhalt
AdminSUPER_ADMIN, CENTER_ADMINOrganisation, Center, Analytics, Kanäle
RedaktionORG_MARKETING_MANAGER, CONTENT_EDITORContent, Social, Workflow, Medien
CM-AppCENTER_MANAGER, CM_ASSISTANT, FACILITY_*, TENANTVor-Ort-Aufgaben + Hinweis manager.cockpit-os.de
Read-onlyVIEWEREinsehen, zugewiesene Bereiche

Redaktions-Digest

AspektVerhalten
Opt-inUser.emailDigestEnabled (Default false)
ZeitzoneEurope/Berlin, Stunde emailDigestHour (7, 8, 9, 10, 12, 15, 18)
FrequenzCron stündlich; Versand nur in gewählter Stunde, max. 1×/Tag (emailDigestLastSentAt)
InhaltGlocke (social_review_approved, social_new_comment), Zähler offene Social-Freigaben + Workflow-Entwürfe
Keine DuplikateSOCIAL_REVIEW_REQUEST + rejected / publish_failed → immer Sofort-Mail; approved → Digest wenn aktiv, sonst Sofort
LeerKein Versand wenn nichts ansteht
CronPOST /api/cron/email-digest — Auth Bearer CRON_SECRET; Query ?dryRun=1, ?userId=
Migrationpackages/database/migrations/20260606140000_add_email_digest_prefs_SAFE.sql

Login: Dashboard = E-Mail + Passwort (NextAuth Credentials). Kein Magic-Link-Login im Dashboard. Magic Links existieren nur für AgencyOS / WordPress-Integration.

Copyright: Footer © {Jahr} SawatzkiMühlenbruch GmbH (Platzhalter im Template, zur Laufzeit EMAIL_BRAND.year); Produktname im Header: cockpitOS.

So testen

  1. RESEND_API_KEY in apps/dashboard/.env (oder .env.local)
  2. pnpm test:dashboard-emails — sendet 18 Template-Varianten an TEST_EMAIL_RECIPIENT (Default intern: sb@schickma.de), Betreff-Prefix [TEST] (inkl. Digest, Social-Anfrage, Client Report, Willkommen CM)
  3. Im Dashboard: Test-User unter Einstellungen → Benutzer & Rollen anlegen → Willkommens-Mail prüfen; schlägt Versand fehl, erscheint eine Warn-Toast, User bleibt angelegt (emailSent: false in API)
  4. Link Account einrichten öffnen → Passwort setzen → Anmeldung mit Hinweis
  5. Passwort zurücksetzen in der User-Liste → 24h-Link, kein Klartext
  6. Optional: Anlegen mit Als Einladung versenden → Einladungs-Template
  7. Statische Verdrahtung (ohne DB/Versand): node scripts/verify-dashboard-email-wiring.mjs
  8. Digest dry-run: pnpm test:email-digest — optional DIGEST_USER_ID=…; Versand: DIGEST_USER_ID=… pnpm test:email-digest -- --send
  9. Cron-Test remote: pnpm test:email-digest -- --cron (benötigt CRON_SECRET)

Betrieb

VariableZweck
RESEND_API_KEYResend API (Pflicht für Versand)
NEXTAUTH_URLBasis-URL für alle Links (z. B. https://dashboard.cockpit-os.de)
RESEND_FROM_EMAILAbsender-Adresse (Default: noreply@mail.cockpit-os.de)
RESEND_FROM_NAMEAnzeigename (Default: cockpitOS)
RESEND_REPLY_TOReply-To für Nutzer-Antworten (Default: support@schickma.de)
CRON_SECRETDigest-Cron + andere Scheduled Jobs (GitHub Actions → Dashboard)

Digest-Workflow: .github/workflows/email-digest.yml — Details: Cron-Setup.

Reply-To in Code: support@schickma.de (env: RESEND_REPLY_TO) — technische Anfragen an Saad Badr, Technischer Leiter. Absender bleibt noreply@mail.cockpit-os.de.

Resend: Versandregion EU (Ireland, eu-west-1) für alle Resend-Domains (u. a. mail.cockpit-os.de) — verifiziert. DPA unterzeichnet (docs/compliance/resend-dpa-signed.pdf). Metadaten/Logs können zusätzlich in den USA verarbeitet werden — in AVV/DSE dokumentieren (SCC).

Domain und DNS für Resend müssen beim Betrieb freigeschaltet sein; ohne gültigen Key schlagen Versand und Testskript fehl.

Tonfall

  • Keine dekorativen Emojis in Betreff, Body oder Buttons — wirkt unprofessionell und „KI-generiert“.
  • Sachliche Anrede, klare Handlungsaufforderung (Link-Button), neutraler Hinweis bei Fristen.
  • UI-Feedback im Dashboard ebenfalls ohne Emoji (Toasts, Alerts) — siehe Projektregel .cursor/rules/no-decorative-emojis.mdc.

Abgrenzung

BereichTemplatesDatei
Dashboard-Benutzerdashboard-email-templates.tsZentrales Layout (email-layout.ts)
Social Freigabe angefordertSOCIAL_REVIEW_REQUESTZentrales Layout
Monatsbericht (extern)CLIENT_REPORT_AVAILABLEZentrales Layout
Center Kontakt / VermietungInline in email.tsCenter-Kontext-Block (Center, Formular, Website-URL), Reply-To = Absender

Center-Website-Formulare (Kontakt / Vermietung)

Versand über send-contact-inquiry bzw. send-rental-inquiry. Jede E-Mail enthält:

FeldQuelle
CenterShoppingCenter.name
FormularformKey → Label (z. B. kontakt, promotionflaeche, vermietung)
WebsiteProduction-URL / Custom Domain / Legacy website (resolveInquiryEmailCenterWebsiteUrl)
BrandingHeader in baseColor, Akzent/Links in accentColor, optional logo — Fallback dunkles Neutralgrau
Betreff (Mail)ILG Kontakt: Besucher-Betreff; Promotion: „Promotionfläche mieten – {Center}
Reply-ToE-Mail des Formular-Absenders

Hilfsmodule: inquiry-email-html.ts (buildBrandedInquiryEmailHtml), contact-inquiry-subject.ts. v0 sendet nur Rohdaten — kein eigenes HTML-Template.

Spam-Schutz (Kontaktformular)

Was: Junk-Mails aus öffentlichen Formularen werden serverseitig gefiltert, bevor Resend sendet. ILG-Formulare (Kontakt, Promotionfläche) haben zusätzlich ein unsichtbares Honeypot-Feld und Cloudflare Turnstile — wie MEC.

Warum: Bots füllen offene Formulare automatisch aus. Center-Manager bekamen dadurch vermehrt Junk im Posteingang.

Wer ist betroffen: Center-Betrieb (weniger Junk), Besucher (unverändertes Formular, ggf. kurzes Turnstile-Widget), Betrieb/IT (Env-Keys).

Wo: Website /kontakt (ILG) bzw. Promotion-Formulare. Versand: POST /api/kontakt (Center-Website) → POST …/send-contact-inquiry (Dashboard).

So testen

  1. ILG-Kontaktseite öffnen, Pflichtfelder ausfüllen, senden — Mail kommt an.
  2. Im DevTools das versteckte Feld website befüllen und erneut senden — UI zeigt Erfolg, es geht keine Mail raus.
  3. Nachricht mit drei oder mehr Links senden — ebenfalls keine Mail.

Betrieb: NEXT_PUBLIC_TURNSTILE_SITE_KEY (Center-Website, öffentlich) und TURNSTILE_SECRET_KEY (Center-Website, nur Server). Ohne Keys bleibt Turnstile aus (Dev); Honeypot und Link-Filter greifen trotzdem. Rate-Limit: 10 Anfragen / 15 Min. pro IP.

Nutzungsstatistik: Seitenaufrufe werden anonymisiert erfasst. Im Umami-Dashboard nach diesem Pfad filtern: /en/developer-guide/dashboard-emails