Die drei letzten wichtigen Features für ein produktionsreifes 1.0.0:
## 1. Passwort vergessen-Flow
Der klassische Selfservice-Reset per Email – sowohl für Mitarbeiter als auch
für Portal-Kunden. User können sich nicht mehr aussperren, Admin muss nicht
mehr manuell eingreifen.
- Neues Link "Passwort vergessen?" auf Login-Seite
- PasswordResetRequest: Email + Typ-Auswahl (Mitarbeiter / Portal)
- PasswordResetConfirm: Token-basierte Bestätigung + neues Passwort (min 6 Zeichen)
- Token ist 2 Stunden gültig, dann muss neu angefordert werden
- Token ist kryptografisch sicher (crypto.randomBytes(32))
- User-Enumeration-Schutz: Backend gibt immer 200 zurück, egal ob Email existiert
- Nach erfolgreichem Reset werden ALLE bestehenden Sessions gekickt
(tokenInvalidatedAt gesetzt) – falls jemand parallel eingeloggt war
DB:
- User.passwordResetToken + passwordResetExpiresAt
- Customer.portalPasswordResetToken + portalPasswordResetExpiresAt
## 2. Rate-Limiting gegen Brute-Force
Mit express-rate-limit:
- Login: 10 Versuche pro 15 Minuten pro IP. Erfolgreiche zählen nicht mit.
- Passwort-Reset-Request: 5 Versuche pro Stunde pro IP (Mail-Flut verhindern)
Sowohl Mitarbeiter-Login als auch Portal-Login geschützt.
## 3. Auto-Geburtstagsgrüße per Cron
Das autoBirthdayGreeting-Flag hatten wir schon, aber kein Scheduler der
ihn wirklich abschickt. Jetzt:
- Läuft täglich um 08:00 Uhr
- Findet Kunden mit heutigem Geburtstag + autoBirthdayGreeting=true
- Nur Email-Kanal (Messenger brauchen Browser-Klick)
- Catch-up 30s nach Server-Start: wenn Server am Geburtstag down war, wird
beim nächsten Boot nachgeholt
- lastBirthdayGreetingYear verhindert Doppelversand
Dependencies: node-cron, @types/node-cron, express-rate-limit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Problem: Das 'Bezeichnung für Kunden-E-Mails'-Feld (UI-Label) war irreführend.
Ein User hat dort die Domain 'stressfrei-meyer.xyz' eingetragen statt ins
Domain-Feld – das eigentliche Domain-Feld blieb unverändert, und das Label
zeigte dann unpassend die Domain-Schreibweise.
Fix: Das Label-Feld ist in 99% aller Fälle nicht nötig, weil es automatisch
aus der Domain abgeleitet wird (stressfrei-wechseln.de → Stressfrei-Wechseln).
Der Edge-Case 'komplett anderer Anzeigetext als aus Domain ableitbar' kommt
selten vor und kann später bei Bedarf über direkten DB-Zugriff/Developer-Panel
gesetzt werden.
Das Schema-Feld bleibt erhalten (für zukünftige Erweiterungen), nur das
Formular-Feld ist weg. Stattdessen Hinweistext unter dem Domain-Feld:
'Wird auch für die Kunden-E-Mail-Adressen genutzt (z.B. name@...)'
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Problem: Nach dem Ändern der Provider-Domain blieb die alte Domain
(stressfrei-wechseln.de) im Adress-Hinzufügen-Dialog bestehen, weil der
Frontend-Hook useProviderSettings() einen 5-Minuten staleTime hat und
nicht invalidiert wurde.
Fix:
- In allen Provider-Mutations (create/update/delete) wird jetzt auch
'email-provider-public-settings' invalidiert → Domain & Label greifen
sofort in allen Komponenten
Zusätzlich Domain-Validierung eingebaut:
- Frontend: pattern am Input + Live-Fehlermeldung
Format: name.tld (mit Subdomains erlaubt, z.B. mail.meine-firma.de)
Input auto-lowercase + trim
- Backend: validateDomain() in createProviderConfig/updateProviderConfig
Wirft Error mit sprechender Meldung bei ungültigem Format
- Schützt vor Versehen im UI + direkten API-Aufrufen
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Alle hardcoded Referenzen auf 'stressfrei-wechseln.de' und 'Stressfrei-Wechseln'
durch dynamische Werte aus der EmailProviderConfig ersetzt. Notwendig für
Multi-Mandanten-Betrieb, wenn das CRM an Dritte vermietet wird.
Schema:
- Neues Feld EmailProviderConfig.customerEmailLabel (String?)
- Wenn leer, wird Label aus Domain abgeleitet ('stressfrei-wechseln.de' → 'Stressfrei-Wechseln')
Backend:
- Neuer Endpoint GET /api/email-providers/public-settings liefert { domain, customerEmailLabel }
- Neue Service-Funktionen: getProviderPublicSettings(), deriveLabelFromDomain()
- create/updateProviderConfig erweitert um customerEmailLabel
Frontend:
- Neuer Hook useProviderSettings() mit Auto-Caching
- Neues Eingabefeld 'Bezeichnung für Kunden-E-Mails' im Provider-Modal
- Dynamische Domain-Suffix im Adress-Hinzufügen-Dialog (@<domain>)
- Tab-Label 'Stressfrei-Wechseln' im Kunden-Detail → dynamisch
- 'Stressfrei-Wechseln Adresse' in ContractForm → dynamisch
- '(Stressfrei-Wechseln)' Badge in ContractDetail → dynamisch
- 'Stressfrei-Wechseln E-Mail' im Generate-Modal → dynamisch
- Leere-Zustand-Meldungen in Tab und E-Mail-Client → dynamisch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bisher blieb ein fehlgeschlagener IMAP-Sync oder E-Mail-Versand still – der User
sah nur im Browser-Devtools, dass etwas schief lief. Jetzt erscheint eine rote
Toast-Benachrichtigung (8 Sekunden) mit der konkreten Fehlermeldung des Servers,
z.B. 'Sync fehlgeschlagen: IMAP-Authentifizierung fehlgeschlagen: NO [AUTHENTICATIONFAILED]'.
EmailClientTab (Synchronisieren-Button):
- toast.success bei erfolgreichem Sync
- toast.error bei Fehler + bei Backend-Response mit success=false
ComposeEmailModal (Senden):
- toast.success bei erfolgreichem Versand
- toast.error bei SMTP-Fehler mit Server-Response (zusätzlich zum Inline-Fehler)
Außerdem im imapService.testImapConnection:
- Roh-Error wird jetzt geloggt (code, response, responseStatus, authenticationFailed)
- ImapFlow-spezifische Felder werden in die Fehlermeldung übernommen, sodass
z.B. '2 NO [AUTHENTICATIONFAILED] Authentication failed.' direkt sichtbar wird
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Das bestehende „Verbindung testen" prüft nur den API-Zugang (Plesk/cPanel),
nicht den eigentlichen IMAP/SMTP-Zugang der System-E-Mail. Das führte dazu,
dass Anhang-Downloads scheiterten obwohl der API-Test grün war.
Neuer Button im EmailProviders-Modal: „E-Mail-Zugang testen (IMAP + SMTP)"
- Testet IMAP-Empfang und SMTP-Versand separat
- Zeigt pro Protokoll Erfolg oder Fehlermeldung mit Server/Port/Verschlüsselung
- Nutzt die hinterlegte System-E-Mail-Adresse + Passwort
- Funktioniert auch vor dem ersten Speichern (mit Formulardaten)
Außerdem im Anhang-Download:
- Retry-Mechanismus bei transienten TLS/Netzwerk-Fehlern (3 Versuche)
- Socket-Timeout 30s gegen hängende Verbindungen
- Sprechende Fehlermeldungen (z.B. Hinweis auf selbstsigniertes Zertifikat)
- Debug-Logging mit Host/Port/User/Folder/UID
Backend:
- Neuer Endpoint POST /api/email-providers/test-mail-access
- fetchAttachment in imapService: Retry-Wrapper + fetchAttachmentInner
- Besseres Error-Handling in downloadAttachment (Cert-Hinweis, Auth, Timeout)
Frontend:
- emailProviderApi.testMailAccess()
- EmailProviders-Modal: neuer Button + zweispaltige Ergebnis-Anzeige für IMAP+SMTP
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ein neues System um Stammdaten-Kataloge zwischen Installationen zu teilen –
explizit ohne Kundendaten, Verträge oder Einstellungen.
**Was wird exportiert:**
- Anbieter + zugehörige Tarife
- Kündigungsfristen
- Vertragslaufzeiten
- Vertragskategorien
- PDF-Auftragsvorlagen (JSON + PDF-Dateien + Feldzuordnungen)
**Was NICHT:**
- Kundendaten, Verträge, Dokumente, Emails, SMTP-Einstellungen
→ dafür gibt es den Datenbank-Backup
**Neue Einstellungsseite /settings/factory-defaults:**
- Zeigt Anzahl pro Kategorie (Anbieter, Tarife, Fristen, …)
- "Exportieren"-Button lädt ZIP herunter (manifest.json + JSONs + PDFs)
- Import-Anleitung inline
**Import-Script:**
- `npm run seed:defaults` (tsx scripts/seed-factory-defaults.ts)
- Liest alle JSON-Dateien aus backend/factory-defaults/*/*.json
- Merged mehrere Dateien automatisch pro Kategorie (unique-key gewinnt zuletzt)
- Upsertet idempotent → kann mehrfach ausgeführt werden
- Kopiert PDF-Vorlagen aus factory-defaults/pdf-templates/ nach uploads/pdf-templates/
- Alte PDF-Dateien werden beim Re-Import entsorgt
Backend:
- services/factoryDefaults.service.ts: collectFactoryDefaults() + exportFactoryDefaults()
- controllers/factoryDefaults.controller.ts: preview + export
- routes/factoryDefaults.routes.ts: GET /api/factory-defaults/preview + /export
- scripts/seed-factory-defaults.ts: CLI-Import-Script
- .gitignore: factory-defaults/* außer .gitkeep und README.md
Frontend:
- pages/settings/FactoryDefaults.tsx: Übersicht + Export-Button
- Settings-Karte „Factory-Defaults" im System-Abschnitt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Das useInformalAddress-Feld war:
1. Im Frontend-Submit-Handler nicht in submitData enthalten (wurde bei jedem Update rausgefiltert)
2. Im Service-Type nicht definiert (TypeScript-mäßig unbekannt)
3. Beim Laden im Edit-Mode: Boolean aus DB matchte nicht das String-value des <select>
Fixes:
- Frontend: submitData enthält jetzt useInformalAddress (String oder Boolean → sauberes Boolean)
- Frontend: beim reset() wird Boolean zu 'true'/'false' konvertiert für <select>
- Backend: Service-Type erweitert um useInformalAddress, autoBirthdayGreeting, autoBirthdayChannel
- Backend: Audit-Feldlabels für die neuen Felder
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Der SaveAttachmentModal hat jetzt drei Modi (wenn E-Mail einem Vertrag zugeordnet ist):
1. Als Dokument – in feste Slots (Kündigungsschreiben etc.), unverändert
2. Als Vertragsdokument – NEU: flexible ContractDocument-Tabelle mit Typ-Dropdown
(Auftragsformular, Lieferbestätigung, Vertragsunterlagen, Vollmacht,
Widerrufsbelehrung, Preisblatt, Sonstiges) + optionalen Notizen
3. Als Rechnung – jetzt für ALLE Vertragstypen (vorher nur Strom/Gas)
Backend:
- Neuer Endpoint POST /api/emails/:id/attachments/:filename/save-as-contract-document
- saveAttachmentAsInvoice + saveEmailAsInvoice: ELECTRICITY/GAS-Einschränkung entfernt,
nutzt jetzt addInvoiceByContract als Fallback für Nicht-Energie-Verträge
Frontend:
- cachedEmailApi.saveAttachmentAsContractDocument hinzugefügt
- SaveAttachmentModal: neuer Mode 'contractDocument' mit Typ+Notizen
- Mode-Toggle zeigt jetzt alle drei Optionen wenn Vertrag zugeordnet
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Neuer Cake-Button neben dem Geburtsdatum in den Stammdaten öffnet ein Modal
mit drei Funktionen:
1. **Gruß-Marker zurücksetzen** (lastBirthdayGreetingYear → null)
- Für Debugging oder als Fallback, wenn der Kunde den Gruß erneut sehen soll
- Mit Bestätigungsdialog
2. **Geburtstagsgruß jetzt senden** (Email / WhatsApp / Telegram / Signal)
- Email: direkt via System-SMTP mit HTML-Template (Du/Sie-abhängig)
- WhatsApp/Telegram/Signal: öffnet vorbefülltes Fenster mit Gruß-Text
- Text beachtet Du/Sie-Verhältnis (pronomen, possessiv, etc.)
- Mit Bestätigungsdialog
3. **Automatisch senden** – neue Einstellung am Customer
- autoBirthdayGreeting (Boolean) + autoBirthdayChannel (String)
- Für späteren Cron-basierten Automatik-Versand vorbereitet
Backend:
- birthday.service.ts: resetBirthdayGreeting, buildBirthdayGreetingText, getBirthdayGreetingData
- birthday.controller.ts: resetBirthdayGreeting, sendBirthdayGreeting
- Routes: POST /birthdays/:customerId/reset + /send
- Audit-Log bei beiden Aktionen
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Schema:
- Customer.useInformalAddress: Boolean (Default: false = Sie)
- Auch bei Firmenkunden verfügbar (Chef kann man auch duzen)
Frontend:
- Neues Pflichtfeld "Anrede per" (Du/Sie) im Kunden-Formular
- Anzeige als Badge in CustomerDetail-Stammdaten
Geburtstagsgruß im Portal:
- Bei Du: "Herzlichen Glückwunsch, Max! Alles Gute zu deinem 42. Geburtstag!"
- Bei Sie: "Herzlichen Glückwunsch, Herr Müller! Alles Gute zu Ihrem 42. Geburtstag!"
- Konsistent auch bei nachträglichen Glückwünschen (hattest/hatten, bist/sind etc.)
- Backend liefert firstName, lastName, salutation und useInformalAddress
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bisher wurden die Felder nur bei Privatkunden angezeigt. Jetzt sind sie
unabhängig vom Kundentyp verfügbar, z.B. für Ansprechpartner bei Firmen.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- backend/dist, backend/node_modules aus Git-Tracking entfernt (waren bereits in .gitignore)
- frontend/dist, frontend/node_modules ebenfalls entfernt
- Neue frontend/.gitignore erstellt (fehlte komplett)
Die Dateien bleiben lokal erhalten und werden durch npm install + build regeneriert.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Admin (Vertrags-Cockpit):
- Neue Section "Geburtstage" zeigt Kunden mit Geburtstag
- Fenster: -7 bis +30 Tage um heute
- Farbcodierung: heute (pink), vergangen (amber), bevorstehend (grau)
- Anzeige: Name, Kundennummer, Geburtsdatum, Alter, "Heute!" / "In X Tagen" / "Vor X Tagen"
Portal (Kundenportal):
- Modal mit Geburtstagsgruß wenn Geburtstag heute oder in den letzten 7 Tagen war
- Unterscheidet zwischen aktuellem Geburtstag und nachträglichen Glückwünschen
- Schönes Gradient-Design mit Konfetti-Emojis
- Wird pro Jahr nur einmal angezeigt (Customer.lastBirthdayGreetingYear)
- Bestätigung speichert das aktuelle Jahr
Backend:
- Neues Feld Customer.lastBirthdayGreetingYear (Int?)
- Service birthday.service.ts mit Fenster-Logik + Alter-Berechnung
- Endpoints /api/birthdays/upcoming (Admin),
/api/birthdays/my-birthday (Portal GET + POST /acknowledge)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- PDF-Template-Editor in Einstellungen: Vorlagen hochladen, Formularfelder automatisch auslesen, CRM-Felder zuordnen
- PDF-Vorschau mit annotierten Feldnamen, seitenweise Sortierung der Felder
- Auftrag generieren aus Vertragsdaten (Button im Vertrags-Detail)
- Dynamische Rufnummern-Felder mit Vorwahl-Extraktion und konfigurierbarer Maximalanzahl
- Nicht zugeordnete Felder bleiben editierbar im generierten PDF
- Eigentümer-Felder mit Namens-Kombinationen (Firma+Name etc.) und Fallback auf Kundendaten
- Stressfrei-E-Mail als Feld-Option im Template-Editor
- Objekttyp, Lage und Lage des Anschlusses als neue Felder bei Festnetz-Verträgen (DSL, Glasfaser, Kabel)
- Bankverbindung-Fallback: wenn keine am Vertrag verknüpft, wird automatisch die neueste aktive des Kunden genommen
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>