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) sind separate Templates in email.ts und unverändert.
Warum
Professioneller SaaS-Standard: Setup- und Reset-Links statt Passwort per E-Mail (Risiko durch Postfach-Zugriff, Weiterleitung, Logs, Support-Screenshots).
Wer ist betroffen
| Gruppe | Inhalt |
|---|---|
| Redaktion / Admins | Willkommen, Einladung, Rollenänderung, Account-Status, Admin-Reset |
| Neue Nutzer | Account einrichten über /auth/activate |
| Entwickler | Templates, API-Hooks, Token-Logik |
| Betrieb / IT | RESEND_API_KEY, NEXTAUTH_URL, Absender-Domain |
| Besucher | nicht betroffen (Center-Mails separat) |
Nutzer-Doku (Bedienung im Dashboard): Benutzer und Organisation.
Wo
Code
| Bereich | Pfad |
|---|---|
| Layout & Copyright | apps/dashboard/src/lib/email-layout.ts |
| Templates (15 Dashboard-Typen) | apps/dashboard/src/lib/dashboard-email-templates.ts |
| Digest-Cron-Logik | apps/dashboard/src/lib/email-digest.ts |
| Versand-Helfer | apps/dashboard/src/lib/email.ts |
| Token, URLs, Platzhalter-PW | apps/dashboard/src/lib/user-account-tokens.ts |
| User-Onboarding-Flows | apps/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 / Reset | apps/dashboard/src/components/auth/account-password-form.tsx |
| Testskript | scripts/test-dashboard-emails.ts → pnpm test:dashboard-emails |
URLs (Dashboard-App)
| Pfad | Zweck |
|---|---|
/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-activated | Erfolg nach Einrichtung |
/auth/signin?message=password-reset-success | Erfolg nach Reset |
API (Auslöser)
| Endpoint | |
|---|---|
POST /api/users | Willkommen oder Einladung (invite: true) |
POST /api/users/{userId}/reset-password | Admin-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-password | Passwort vergessen (Self-Service; gleiche Token-/Resend-Pipeline wie Admin-Reset) |
POST /api/auth/reset-password | Bestätigung Passwort geändert (nach Token-Reset) |
POST /api/users/me/change-password | Bestätigung Passwort geändert (Self-Service) |
| Social-Workflow | SOCIAL_REVIEW (abgelehnt / Publish-Fehler sofort; freigegeben nur sofort wenn Digest aus) |
POST /api/cron/email-digest | EMAIL_DIGEST (täglich, opt-in, nur bei Inhalt) |
GET/PATCH /api/users/me/notification-preferences | Digest ein/aus + Versandstunde |
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)
| ID | Betreff-Kontext | Link / Gültigkeit |
|---|---|---|
PASSWORD_RESET | Passwort vergessen | /auth/reset-password — 24 h |
WELCOME | User angelegt | /auth/activate (purpose=setup) — 7 Tage, auch bei Inaktiv |
USER_INVITATION | Einladung (invite: true) | /auth/activate (purpose=setup) — 7 Tage |
PASSWORD_CHANGED | Nach erfolgreichem Reset | — |
ACCOUNT_LOCKED | Nach 5 Fehlversuchen (Login) | /auth/unlock (24 h) |
ROLE_CHANGE | Rolle geändert | — |
PASSWORD_RESET_BY_ADMIN | Admin-Reset | /auth/reset-password — 24 h |
ACCOUNT_DEACTIVATED | Account deaktiviert | — |
ACCOUNT_REACTIVATED | Account reaktiviert | Link zur Anmeldung |
ACCOUNT_DELETED | Account gelöscht | — |
SOCIAL_REVIEW | Social-Freigabe-Workflow | Dashboard-URLs zum Post |
EMAIL_DIGEST | Tägliche Redaktions-Zusammenfassung | Einstellungen + Dashboard |
Redaktions-Digest
| Aspekt | Verhalten |
|---|---|
| Opt-in | User.emailDigestEnabled (Default false) |
| Zeitzone | Europe/Berlin, Stunde emailDigestHour (7, 8, 9, 10, 12, 15, 18) |
| Frequenz | Cron stündlich; Versand nur in gewählter Stunde, max. 1×/Tag (emailDigestLastSentAt) |
| Inhalt | Glocke (social_review_approved, social_new_comment), Zähler offene Social-Freigaben + Workflow-Entwürfe |
| Keine Duplikate | rejected / publish_failed → immer Sofort-Mail; approved → Digest wenn aktiv, sonst Sofort |
| Leer | Kein Versand wenn nichts ansteht |
| Cron | POST /api/cron/email-digest — Auth Bearer CRON_SECRET; Query ?dryRun=1, ?userId= |
| Migration | packages/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
RESEND_API_KEYinapps/dashboard/.env(oder.env.local)pnpm test:dashboard-emails— sendet 14 Templates anTEST_EMAIL_RECIPIENT(Default:sb@schickma.de), Betreff-Prefix[TEST]- Im Dashboard: Test-User unter Einstellungen → Benutzer & Rollen anlegen → Willkommens-Mail prüfen
- Link Account einrichten öffnen → Passwort setzen → Anmeldung mit Hinweis
- Passwort zurücksetzen in der User-Liste → 24h-Link, kein Klartext
- Optional: Anlegen mit Als Einladung versenden → Einladungs-Template
- Statische Verdrahtung (ohne DB/Versand):
node scripts/verify-dashboard-email-wiring.mjs - Digest dry-run:
pnpm test:email-digest— optionalDIGEST_USER_ID=…; Versand:DIGEST_USER_ID=… pnpm test:email-digest -- --send - Cron-Test remote:
pnpm test:email-digest -- --cron(benötigtCRON_SECRET)
Betrieb
| Variable | Zweck |
|---|---|
RESEND_API_KEY | Resend API (Pflicht für Versand) |
NEXTAUTH_URL | Basis-URL für alle Links (z. B. https://dashboard.cockpit-os.de) |
RESEND_FROM_EMAIL | Absender-Adresse (Default: noreply@mail.cockpit-os.de) |
RESEND_FROM_NAME | Anzeigename (Default: cockpitOS) |
CRON_SECRET | Digest-Cron + andere Scheduled Jobs (GitHub Actions → Dashboard) |
Digest-Workflow: .github/workflows/email-digest.yml — Details: Cron-Setup.
Reply-To in Code: support@cockpit-os.de.
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
| Bereich | Templates | Datei |
|---|---|---|
| Dashboard-Benutzer | dashboard-email-templates.ts | Zentrales Layout |
| Center Kontakt / Vermietung | Inline in email.ts | Unverändert, kein SaaS-Layout-Refactor |
Nutzungsstatistik: Seitenaufrufe werden anonymisiert erfasst. Im Umami-Dashboard nach diesem Pfad filtern: /developer-guide/dashboard-emails