Commit graph

10 commits

Author SHA1 Message Date
Claude
17267b7c05 KC@FlessingLabs 1.0.0: Testmodus ohne Lizenz, Installationsanleitung, Nutzungsbedingungen
Testmodus (ohne gültige Lizenz): 1 Personal-Konto, 2 Kunden; Discord-Bot,
E-Mail-Posteingang (IMAP), DATEV-Export und Backups auf externe Ziele nur mit
Lizenz. Mit Lizenz gelten deren Grenzen und Module (Lizenz ohne Modulliste =
voller Umfang). Bestehende Daten bleiben bei Ablauf vollständig nutzbar, nur
Neuanlagen über die Grenzen und die Zusatzfunktionen pausieren.

- Edition zentral in @kc/platform/license (getEdition, hasFeature,
  assertCustomerCapacity, assertStaffCapacity), auch in der CLI durchgesetzt
- Einstellungen → Lizenz zeigt die Edition, Hinweisbanner im Testmodus
- README mit Installation, Update und Betrieb; systemd-Units unter ops/systemd
- .env.example bereinigt (Kommentare nicht mehr hinter Werten, veraltete
  SMTP/Discord-Variablen entfernt; beides wird in der Oberfläche eingestellt)
- LICENSE.md: Nutzungsbedingungen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:31:53 +02:00
Kundencenter
dad9528295 Add contract renewal reminder emails (keep/cancel decision links)
Sends a one-time email ~30 days before any contract's term_end
(whether renewal='auto' or 'none'), with "Behalten" and "Kündigen"
action links. "Behalten" extends the contract by one renewal term;
"Kündigen" sets the same cancellation effective-date logic as the
customer-facing cancel flow and opens a staff ticket so the external
deregistration (e.g. with a domain registrar) actually gets done.

Uses a dedicated single-use token table (contract_renewal_tokens)
since contracts, unlike user_tokens, are not scoped to a single user.
The public confirmation page lives at /vertrag-entscheidung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-01 11:51:19 +02:00
Kundencenter
d033088223 feat(license): Kundencenter-Lizenzierung gegen licensing.flessinglabs.com (#116)
Milestone "Kundencenter lizenzierbar machen": die Installation prüft jetzt
ihre eigene Lizenz beim Lizenzsystem (licensing.flessinglabs.com, Programm
"Kundencenter") und setzt die Kundengrenze serverseitig durch - nicht zu
verwechseln mit dem bestehenden "licensing"-Connector, der KUNDEN-Lizenzen
verwaltet, die das Kundencenter weiterverkauft.

- Migration 035: license_state (ein Lizenzschlüssel, verschlüsselt wie
  andere Secrets, stabile instance_id, zuletzt geprüfter Entitlement-
  Snapshot), license_signing_keys (öffentliche Signaturschlüssel, werden
  nur ergänzt, nie anhand einer Serverantwort gelöscht).
- @kc/platform/license: checkLicenseNow() ruft POST /license/verify mit
  X-Program-Key auf, prüft die FLS1-Signatur (RSA-2048, PKCS#1 v1.5,
  SHA-256) und schreibt das Ergebnis in license_state. getEntitlement()
  liefert den nutzbaren Stand inkl. Offline-Fenster (Standard 7 Tage,
  verlängert nie eine tatsächlich abgelaufene Lizenz). assertCustomerCapacity()
  sperrt neue/reaktivierte Kunden transaktional (GET_LOCK gegen den letzten
  freien Platz) bei erreichter Kundengrenze oder ungültiger Lizenz - aber
  NICHT, solange die Lizenzierung noch gar nicht konfiguriert oder noch nie
  geprüft wurde (sonst wäre jede frische Installation von Anfang an blockiert).
- Durchsetzung an allen drei Stellen, die einen neuen Kundenplatz belegen:
  Kundenanlage, Reaktivierung (closed→active) und KeyHelp-Import neuer
  (nicht verknüpfter) Kunden.
- Worker prüft alle 12 h automatisch (zusätzlich zur Prüfung beim Speichern
  eines neuen Schlüssels). API: GET/PUT /admin/license/settings,
  POST /admin/license/check. UI: Einstellungen → Lizenz (Status, Module,
  Kundengrenze, Schlüssel eingeben, manuell prüfen).

Beim Testen einen echten Bug gefunden und behoben: Buffer.from(hex,'hex')
verwirft bei ungerader Hex-Länge stillschweigend die letzte Ziffer (der
Exponent "10001"/65537 wurde fälschlich zu 0x1000) - hätte jede Signatur-
prüfung unbemerkt scheitern lassen. Mit geradem Padding gegen echte
Serverantworten (gültig und manipuliert) verifiziert.

Gegen die echte Produktions-API getestet (Program-Key aus LICENSE_PROGRAM_KEY):
Status/Settings/Check-Endpunkte, Signaturprüfung mit echten Schlüsseln
erfolgreich, Kundenanlage korrekt blockiert bei geprüfter ungültiger Lizenz,
danach wieder sauber in den unkonfigurierten (nicht blockierenden) Zustand
zurückgesetzt, da noch kein echter Lizenzschlüssel für diese Installation
vorliegt. Testsuite 58/59 (eine vorbestehende, unabhängige datumsabhängige
Flakiness in orders.test.ts, auch ohne diese Änderungen reproduzierbar).
Audit-Kette und Backup+Wiederherstellungstest danach weiterhin grün.

Offen: Lizenzschlüssel für diese Installation selbst anlegen (Admin-Panel
licensing.flessinglabs.com) und unter Einstellungen > Lizenz eintragen.
Modul-Gates (billing/domains/provisioning/discord/automation) an den
einzelnen Funktionsbereichen sowie die Lizenzübersicht-Admin-UI (#117)
sind noch zu bauen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-01 10:48:48 +02:00
Kundencenter
3240e6df79 feat(audit): Aufbewahrungs-Policy mit Checkpoint-Löschung und CSV-Export
Zweiter Teil von #34: Aufbewahrungsfristen und Export waren offen. Die
DSGVO selbst schreibt keine feste Zahl vor (nur den Grundsatz der
Speicherbegrenzung, Art. 5 Abs. 1 lit. e) - daher eine eigene, änderbare
Policy statt eines hart codierten Gesetzeswerts. Voreinstellung 180 Tage
als verbreitete Praxis-Richtgröße für sicherheitsrelevante Protokolldaten.

- Migration 034: audit_settings (retention_days, Default 180) und
  audit_retention_checkpoints.
- purgeAuditRetention() (@kc/platform/audit): löscht Einträge jenseits
  der Frist. Da jede Zeile die vorherige mit hasht, würde einfaches
  Löschen die Kette brechen - ein Checkpoint (Hash der zuletzt gelöschten
  Zeile) macht sie trotzdem weiter überprüfbar. Bricht nur ab, wenn die
  Kette vorher schon fehlerhaft ist, oder bei einer Lücke zwischen
  gelöschten und verbleibenden Zeilen (unerwartete Zeitstempel-
  Reihenfolge). audit_events ist per Trigger unveränderlich (Migration
  033) - der DELETE-Trigger wird dafür kurz entfernt und sofort wieder
  angelegt, alles unter derselben Sperre wie audit() (re-entrant über
  dieselbe Verbindung, sonst Deadlock). Die Aufräumung selbst wird als
  eigener Audit-Eintrag protokolliert.
- verifyAuditChain() beginnt nach einer Aufräumung beim Checkpoint-Hash
  statt der Genesis-Null - nur wenn die neue erste Zeile auch wirklich
  genau darauf verweist, sonst bliebe eine echte Manipulation unentdeckt.
- Worker: täglicher automatischer Lauf. CLI: audit-purge zum manuellen
  Anstoßen. API: GET/PUT /admin/audit/settings, POST /admin/audit/purge
  (settings.write), GET /admin/audit/export.csv (audit.read, mit
  demselben Formel-Injection-Schutz wie der Rechnungs-Export). Web:
  Einstellungs-Card unter Audit-Protokoll mit Erklärtext zur DSGVO-
  Rechtsgrundlage, Frist-Eingabe, "Jetzt aufräumen" und Export-Link.

Verifiziert: Testsuite (59/59), echter Lauf mit künstlich vordatierten
(aber korrekt verketteten) Testzeilen gegen die Testdatenbank - Kette vor
und nach der Aufräumung intakt, Checkpoint korrekt genutzt, zweiter Lauf
idempotent. Echte HTTP-Endpunkte (inkl. CSRF) gegen Produktion getestet.
Volles Backup+Wiederherstellungstest danach weiterhin grün (201
Einträge).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:01:00 +02:00
Kundencenter
acf8751f9f fix(audit): GET_LOCK-Ergebnis prüfen, connector/ip in Hash-Kette
Zwei vom Nacht-Agenten gefundene Lücken, gegen den echten Code
verifiziert und behoben:

1. Das Ergebnis von GET_LOCK() wurde nie geprüft. Bei Timeout (10s)
   oder Fehler lief audit() trotzdem ungesperrt weiter – zwei
   gleichzeitige Aufrufe hätten dieselbe "letzte Zeile" lesen und
   beide anhängen können, was die Kettengarantie bricht. Wirft jetzt
   einen Fehler, wenn die Sperre nicht erlangt wurde.

2. connector und ip wurden zwar in audit_events gespeichert, aber nie
   mitgehasht – beide Felder ließen sich im Nachhinein unbemerkt
   ändern, ohne die Kette zu brechen. Neue Einträge (hash_version = 2,
   Migration 030) hashen sie jetzt mit. Bestehende Einträge (Version 1)
   bleiben mit ihrer ursprünglichen Formel gültig; verifyAuditChain()
   berücksichtigt die Version pro Zeile.

Gegen die echte Datenbank verifiziert: Kette bleibt über alle 183
bestehenden (v1) Einträge sauber, ein neu eingefügter (v2) Eintrag mit
connector/ip ebenfalls – 184/184 ohne Bruch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 09:56:46 +02:00
Kundencenter
3371655ca3 feat(mail): Begrüßungsmail mit Zugang, Produkten, Preisen und Discord
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>
2026-09-28 23:50:53 +02:00
Kundencenter
8616e51adc feat(mail): HTML-Mails mit Link zum Ticket/zur Rechnung als Schaltfläche
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>
2026-09-28 23:00:31 +02:00
Kundencenter
7d2b2947f4 feat: separate Absenderprofile für Kundencenter/Störungsticket/Rechnungen
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>
2026-09-28 13:16:42 +02:00
Kundencenter
a68b4cf6ec feat: SMTP-Stack mit konfigurierbaren Mailvorlagen
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>
2026-09-28 12:53:29 +02:00
Kundencenter
4763548bfb Stand vor Einführung des Nacht-Agenten 2026-09-27 00:51:32 +02:00