Vom Nightbot-Nachtriage-Durchlauf gefunden (#100) und vom Nutzer bestätigt:
create-superadmin --password=… landete im Klartext in argv – für jeden
lokalen Nutzer über `ps aux` und in der Shell-History sichtbar.
- create-superadmin fragt das Passwort jetzt interaktiv ohne Echo ab (wie
`passwd`), zweimal zur Bestätigung. Für Skripte/Automatisierung optional
über die Umgebungsvariable KC_CLI_PASSWORD (landet nicht in argv/ps,
nur im Prozess-Environ des ausführenden Nutzers/root).
- Neuer Befehl reset-password --email=… : bisher gab es KEINEN Weg, das
Passwort eines bestehenden Nutzers (insb. eines ausgesperrten
Superadmins) zurückzusetzen außer über die Selbstbedienung per E-Mail-
Link (die bei fehlendem Postfachzugriff oder noch nicht eingerichtetem
SMTP nicht hilft) oder direkte DB-Manipulation. Funktioniert für jeden
Nutzer (Kunde oder Personal), setzt failed_logins/locked_until zurück,
protokolliert im Audit-Log.
Beide Befehle scheitern jetzt sauber (Fehlermeldung + Exit-Code, kein
Stack-Trace) statt mit --password=<12+ Zeichen> zu arbeiten.
Gegen die Testdatenbank durchexerziert: Anlegen, doppelte E-Mail, zu
kurzes Passwort, Reset, unbekannte E-Mail, fehlendes TTY ohne
KC_CLI_PASSWORD – alle Pfade verhalten sich wie erwartet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Beim Nightbot-Nachtriage-Durchlauf gefunden: Firmenname und Notiz im
Rechnungsjournal-CSV (aus Kunden-/Personal-Eingaben) landeten ungeprüft
in der Zelle. Beginnt ein solcher Wert mit =, +, - oder @, interpretieren
Excel/Sheets ihn beim Öffnen als Formel statt als Text (CSV-Injection).
Zellen mit einem dieser Startzeichen werden jetzt mit einem führenden
Apostroph entschärft (Standard-Gegenmaßnahme), sowohl im generischen
CSV-Export als auch im DATEV-Buchungstext (dort fließt der Firmenname
ebenfalls mit ein).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neue Seite Rechnungen → Export mit Zeitraum-/Status-Filter und drei
Downloads, nie Entwürfe:
- Rechnungsjournal als CSV (Semikolon, Dezimalkomma, UTF-8-BOM):
Nummer, Datum, Kunde, Netto/USt/Brutto, effektiver USt-Satz, Status,
Zahlungsdatum, aufgelöste Storno-Referenzen.
- Alle zugehörigen Rechnungs-PDFs als ZIP (die eigentlichen Belege,
wie von GoBD neben dem Journal verlangt), gebaut über das lokale
zip-Binary wie in ops/backup.ts.
- Echter DATEV-Buchungsstapel (EXTF, Format 700/21) zum direkten
Import beim Steuerberater. Pro Rechnung eine Buchungszeile je
USt-Satz-Gruppe (Automatikkonten-Verfahren: Konto = aus der
Kundennummer abgeleitetes Debitorenkonto, Gegenkonto = konfiguriertes
Erlöskonto des jeweiligen Satzes – kein geratener BU-Schlüssel
nötig). Kopf- und Buchungszeilen-Layout (31 bzw. 125 Felder) gegen
die offizielle DATEV-Formatbeschreibung und ein reales Beispiel aus
github.com/ledermann/datev verifiziert, nicht aus dem Gedächtnis
geraten. Ausgabe in Windows-1252 (eigener Encoder: Node kennt nur
striktes ISO-8859-1, das den Halbgeviertstrich in den DATEV-
Spaltennamen stillschweigend verstümmelt hätte – beim Testlauf
gefunden und behoben).
Dafür neue DATEV-Stammdaten unter Einstellungen → Firma (Berater-/
Mandantennummer, SKR, Sachkontenlänge, Wirtschaftsjahresbeginn,
Erlöskonten je Steuersatz; Migration 031). Ohne diese Angaben liefert
der DATEV-Export eine klare Fehlermeldung statt einer falschen Datei;
das CSV/ZIP funktioniert unabhängig davon immer.
Gegen echte Rechnungen (RE-1001/RE-1002, Original + Storno) über eine
temporäre Test-Session verifiziert: CSV/ZIP/DATEV liefern korrekte
Inhalte, Storno kehrt Soll/Haben korrekt um, Feldzahlen exakt 31/125,
Encoding rund-trip-fest.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Vom Nacht-Agenten gefundene Lücke (P1), gegen den echten Code verifiziert:
das Backup-Archiv enthielt nur db.sql/config/manifest.json, nicht das
Anhänge-Verzeichnis (STORE_ROOT, /var/lib/kundencenter/ticket-attachments).
Bei einem echten Datenverlust wären alle Ticket-Anhänge unwiederbringlich
verloren gewesen, obwohl die Datenbank-Referenzen darauf erhalten blieben.
- runBackup: Anhänge-Verzeichnis wird mit in archive.tar.gz gepackt (eigener
-C-Abschnitt, da es außerhalb des Arbeitsverzeichnisses liegt), Datei-/
Byte-Anzahl landet zur Kontrolle im manifest.json.
- runRestoreTest: prüft nach dem Entpacken, ob Datei- und Byte-Anzahl der
Anhänge mit dem Manifest übereinstimmen (wie die bestehenden Zeilenzahl-/
Migrations-/Audit-Kette-Prüfungen). Ältere Sicherungen ohne dieses Feld
werden übersprungen statt fälschlich als fehlerhaft gemeldet.
Gegen eine echte Sicherung+Wiederherstellungstest verifiziert (zwei
Testdateien im Anhänge-Verzeichnis angelegt, Backup gefahren, Restore-Test
lief grün mit anhaengeDateien: 2).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nutzerfrage: warum gibt es neben der Kontoverknüpfung noch eine
separate "Erlaubte Nutzer-IDs"-Liste, und woher weiß der Bot, ob
jemand Kunde oder Personal ist? Antwort: das waren zwei parallele,
verwirrende Mechanismen. /kc-status und /kc-kunde prüften bisher eine
manuell gepflegte Discord-ID-Liste (aus der Zeit vor der OAuth-
Verknüpfung), während /zuweisen und /schliessen bereits das verknüpfte
Personal-Konto (users.discord_user_id, kind='staff') prüften.
Jetzt einheitlich: alle vier Personal-Befehle verlangen ein verknüpftes
Personal-Konto, genau wie /zuweisen und /schliessen. Die Liste entfällt
vollständig (Migration 029, Spalte staff_user_ids entfernt) – wer ein
Personal-Konto im Kundencenter hat und es unter "Mein Konto" mit
Discord verknüpft, darf automatisch alle vier Befehle nutzen, ohne
zusätzliche manuelle Pflege einer ID-Liste.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neue Mailvorlage "welcome" (Migration 028), von Personal gezielt über
einen Button auf der Kundenseite ausgelöst ("Begrüßungsmail senden"),
statt automatisch bei der Kundenanlage – sinnvoll ist sie erst, sobald
Produkte zugeordnet sind.
Enthält: Begrüßung, Kundennummer, eine Klartext-Aufstellung der
aktuellen aktiven Produkte mit Preisen (Privatkunden brutto,
Geschäftskunden netto+brutto, wie im Rest der Oberfläche), eine kurze
Anleitung (Ressourcen/Tickets/Rechnungen) und einen Hinweis zur
Discord-Verknüpfung unter Mein Konto. Zugangslink ist je nach Status
entweder ein frischer Einladungslink (noch nicht aktiviert) oder der
normale Login-Link (bereits aktiv).
Außerdem: renderTemplateHtml verlinkt jetzt auch weitere nackte URLs
im Text automatisch (nicht nur die Haupt-Link-Variable), damit z. B.
der Discord-Hinweis in der HTML-Ansicht klickbar ist.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Zweiter Teil des Discord-Ausbaus. Hat ein Kunde sein Konto verknüpft
(siehe vorheriger Commit), bekommt sein Ticket automatisch einen
privaten Thread im konfigurierten Ticket-Kanal:
- Neuer Job discord.ticket_sync: legt den Thread beim ersten Bedarf
an (Name "ticket-<Nummer>", Kunde wird als Mitglied hinzugefügt)
und postet dort jede neue, nicht-interne Nachricht (sowohl Kunden-
als auch Personal-Antworten). Wird von /tickets (Erstellung durch
Kunden) und /tickets/:id/messages (Antworten, keine internen
Notizen) ausgelöst.
- Umgekehrte Richtung: Antwortet der Kunde direkt im Thread, übernimmt
ein neuer messageCreate-Listener im Bot die Nachricht als
Ticket-Antwort – nur wenn der Absender über discord_user_id
eindeutig als Mitglied der Organisation des Tickets verifiziert ist.
Personal-Nachrichten im Thread werden bewusst nicht übernommen (Web-
Oberfläche bleibt die Quelle für Personal-Antworten, kein Echo).
- Dafür nötig: privilegierte Message Content Intent (war in der
Anleitung aus dem vorigen Schritt schon vermerkt).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Erster Teil des größeren Discord-Ausbaus (Konto verknüpfen, Tickets
über Discord sehen/beantworten). Dieser Schritt: Kunden können unter
"Mein Konto" ihr Discord-Konto per OAuth verknüpfen/trennen.
- Migration 027: discord_settings um client_id/client_secret_enc/
ticket_channel_id erweitert; users um discord_user_id/
discord_username (unique); discord_oauth_states (kurzlebiger
CSRF-Schutz für den Redirect-Flow, 10 Min. gültig); neue Tabelle
ticket_discord_threads für den nächsten Schritt (Ticket-Threads).
- Neue Routen: GET/POST /discord/link, /discord/unlink,
GET /discord/oauth/start (leitet zu Discord weiter),
GET /discord/oauth/callback (tauscht Code gegen Discord-Nutzer-ID,
verknüpft das Konto, verhindert doppelte Verknüpfung).
- Einstellungen > Discord: neue Felder für Client ID/Secret, Anzeige
der einzutragenden Redirect-URI, aktualisierte Anleitung inkl.
OAuth-Einrichtung und Hinweis auf die Message Content Intent (für
den nächsten Schritt: Ticket-Threads).
Noch offen (nächster Schritt): Ticket-Threads je Ticket, bidirektionale
Synchronisierung der Nachrichten.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bisher kam die Discord-Konfiguration (Token, Server-ID, Kanal-ID,
erlaubte Nutzer-IDs) ausschließlich aus Umgebungsvariablen. Jetzt
unter Einstellungen > Discord einstellbar, inkl. Schritt-für-Schritt-
Anleitung zum Einrichten einer Discord-Anwendung/eines Bots (Token
erzeugen, einladen, IDs ermitteln).
- Migration 026: discord_settings (Token verschlüsselt, wie SMTP/IMAP).
- apps/worker/src/discord.ts liest die Einstellungen jetzt aus der DB
statt aus process.env; syncDiscord() läuft periodisch (60s) und
verbindet automatisch neu, wenn sich Token/Server/Aktivierung
geändert haben (Trennung von Verbindungs- und Laufzeitdaten: Kanal-ID
und erlaubte Nutzer werden bei jeder Nutzung frisch aus der DB
gelesen, ohne Neuverbindung).
- Neues API-Modul apps/api/src/modules/discord: Einstellungen lesen/
schreiben, Token- und Testnachricht-Prüfung direkt über die Discord-
REST-API (unabhängig vom laufenden Bot-Prozess im Worker).
- Rechte discord.read (Admin+) / discord.write (Superadmin), wie beim
E-Mail-Modul.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mails wurden bisher nur als Klartext verschickt. sendMail/sendTemplateMail
verschicken jetzt zusätzlich eine einfache, mandantenfarbige HTML-Version
(renderTemplateHtml in @kc/platform/mail): ein Platzhalter, dessen Name auf
"Link" endet, wird darin zu einer Schaltfläche statt einer nackten URL; der
Klartext-Teil behält den Link weiterhin sichtbar (Fallback für Mailprogramme
ohne HTML-Ansicht).
ticket_created/ticket_message bekommen dafür eine neue Variable
{{ticketLink}}, invoice_issued {{invoiceLink}} (Migration 025 – nur
Vorlagen mit noch unverändertem Text werden angepasst, damit von Personal
bereits bearbeitete Texte nicht überschrieben werden).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Beim Antworten auf ein Kunden-Ticket verschickte das System schon
automatisch (unsichtbar) eine Benachrichtigung an den Kunden. Jetzt
gibt es dafür eine Checkbox "Kunde per E-Mail benachrichtigen"
(Personal, Standard: an) im Antwortformular, und nach dem Absenden
eine Bestätigung ("Antwort gesendet, Kunde wurde per E-Mail
benachrichtigt."), statt dass der Versand unsichtbar im Hintergrund
passiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neues Postfach lässt sich unter Einstellungen > E-Mail > IMAP-Posteingang
einrichten (Host/Port/Verschlüsselung/Zugangsdaten/Ordner, An/Aus,
Verbindungstest). Der Worker ruft es alle 2 Minuten ab (mailbox.ts) und
ordnet eingehende Mails zu:
1. Betreff enthält eine Ticketnummer (T-123) UND Absender gehört
nachweislich zum Kunden dieses Tickets -> wird als Kundennachricht
angehängt (Status pending_staff, Discord-Benachrichtigung).
2. Sonst, wenn der Absender ein bekannter aktiver Kunde ist -> neues
Ticket wird automatisch unter seinem Kundenkonto angelegt.
3. Sonst (unbekannter/nicht verifizierbarer Absender) -> landet im
neuen Posteingang (/admin/inbox, UI unter /admin/posteingang) zur
manuellen Zuordnung: Personal wählt einen Kunden (legt Ticket an),
hängt die Mail an ein bestehendes Ticket, oder ignoriert sie.
Beim ersten Aktivieren wird kein Mailbox-Bestand verarbeitet, nur ab
dann neu Eingehendes (last_uid wird auf den aktuellen Stand gesetzt).
Migration 024: imap_settings (Verbindung, verschlüsseltes Passwort,
last_uid/last_error), inbox_messages (Warteschlange). Rechte:
email.read/write für die IMAP-Verbindung (wie SMTP), tickets.read/write
für den Posteingang (Zuordnung ist eine Ticket-Aktion, daher im
Tickets-Modul statt eines eigenen Moduls, um keine Modulgrenzen zu
verletzen).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bisher gab es nur einen globalen SMTP-Absender für alle Mailvorlagen.
Jetzt kann je Bereich ein eigener Absendername/-adresse hinterlegt
werden (z. B. support@… für Tickets, rechnung@… für Rechnungen);
leer gelassene Felder fallen weiter auf den globalen SMTP-Absender
zurück. Jede Vorlage ist fest einem Bereich zugeordnet (Migration
023: mail_templates.category, neue Tabelle mail_sender_profiles mit
den drei Zeilen kundencenter/stoerungsticket/rechnungen).
Außerdem: der Testmail-Versand (SMTP-Verbindung wie auch je Vorlage)
zeigt jetzt den konkreten Fehler aus dem Mail-Protokoll an (z. B.
"535 5.7.8 authentication failed") statt nur "Fehlgeschlagen".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bisher gab es nur zwei fest im Code verdrahtete E-Mails (Einladung,
Passwort-Reset) und die SMTP-Verbindung kam ausschließlich aus
Umgebungsvariablen. Jetzt:
- SMTP-Verbindung unter Einstellungen > E-Mail konfigurierbar (Host,
Port, Verschlüsselung, Zugangsdaten verschlüsselt gespeichert,
Absender), mit Testmail-Versand. Die Mail-Logik wandert dafür nach
@kc/platform/mail, damit API und Worker sie gemeinsam nutzen.
- Mailvorlagen-Verwaltung: jedes Ereignis hat eine feste "Definition"
(Name, Auslöser, verfügbare Platzhalter/Daten), Betreff/Text sind
editierbar, je Vorlage einzeln aktivierbar, mit Testversand anhand
von Beispieldaten. Unbekannte Platzhalter werden beim Speichern
abgelehnt.
- Neue Ereignisse verdrahtet: Ticket erstellt (Bestätigung an Kunde),
Personal antwortet auf Ticket (Benachrichtigung an Kunde), Rechnung
ausgestellt, sowie die zuvor zurückgestellte Funktion "Panel-
Zugangsdaten per E-Mail senden" nach einem Passwort-Reset. Diese
laufen über die Jobqueue (mail.template), analog zu discord.notify.
- Versandprotokoll (letzte 100 Versuche, ohne Inhalte) einsehbar unter
Einstellungen > E-Mail.
- Migration 022, Rechte email.read (Admin+) / email.write (Superadmin).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>