Eviworx
Docs

Privacy & DSGVO

Der Bereich Privacy bündelt alle Betroffenenrechte und die technische Datensparsamkeit an einer Stelle: Datenexport (Art. 15/20), Anonymisierung (Art. 17), zeitbasierte Aufbewahrung (Retention) und die öffentliche Transparenz (Datenschutzerklärung, Impressum). Endpunkte unter /api/privacy; öffentliche Teile unter /api/public.

🛡️
Funktionen
✓ Datenexport als JSON (Art. 15/20)
✓ Self-Service-Export (1 pro 24 h)
✓ Admin-/DSB-Export für jede Person
✓ Export in der Sprache der betroffenen Person
✓ Unwiderrufliche Anonymisierung (Art. 17)
✓ Vorgänge bleiben vollständig (anonymer Platzhalter)
✓ Vorabprüfung mit Liste der Hindernisse
✓ Lösch-Sperre mit Pflicht-Begründung (Legal Hold)
✓ Lösch-Rückstand mit Kennzahlen und Filtern
✓ Nächtliche Aufbewahrungsfristen (retention_purge)
✓ Datenschutzerklärung mit Live-Fristen, Impressum

Endpoints

MethodEndpointPermissionAntwortBeschreibung
GET/api/privacy/export/mejede Anmeldung (nur eigene Daten)200, DokumentSelf-Service-Export, höchstens 1 pro 24 h
GET/api/privacy/export/:userIdusers.dataExport200, DokumentExport für eine beliebige Person, ohne Kontingent
GET/api/privacy/users/:id/erasure-checkusers.erase200, PrüfergebnisVorabprüfung: Hindernisse, Sperr- und Archivstatus
POST/api/privacy/users/:id/eraseusers.erase204, ohne InhaltAnonymisierung (irreversibel)
POST/api/privacy/users/:id/erasure-holdusers.erase204, ohne InhaltLösch-Sperre setzen/aufheben
GET/api/privacy/erasure-backlogusers.eraseaudit.enterpriseView{ data, pagination }Lösch-Rückstand als Liste: überfällige archivierte Konten und inaktive E-Mail-Kontakte, mit Suche, Filtern und Seiten
GET/api/privacy/erasure-backlog/statsusers.eraseaudit.enterpriseView{ stats, retention }Kennzahlen desselben Rückstands (ungefiltert) und die geltenden Fristen
GET/api/public/privacy-configkeine{ retention }Live-Fristen für die Datenschutzerklärung
GET/api/public/imprintkeine200, ImpressumsdatenImpressumsdaten (auch vor Login)

Alle Endpunkte unter /api/privacy verlangen eine angemeldete Person; ein API-Key wird abgelehnt (403). Der Grund: Betroffenenrechte sind Entscheidungen, die einem Menschen zurechenbar bleiben müssen — der Audit-Eintrag nennt ihn. Die Prüf- und Rückstandsantworten kommen ohne Hülle, Anonymisierung und Lösch-Sperre antworten 204 ohne Inhalt; eine unbekannte Person ist 404.

Datenexport (Art. 15/20)

Der Export sammelt alle personenbezogenen Daten einer Person in ein maschinenlesbares JSON (Art.-20-tauglich) — Stammdaten, Vorgänge (Tickets als Kunde/Beteiligter, Approvals, Abwesenheiten, Handovers, Assets/Lizenzen), Kommunikation (E-Mails, Web-Notifications, Teams-Referenz) und Meta-Daten (Aktivitäts-/Audit-Einträge als Akteur, wobei bereits redigierte Felder redigiert bleiben). Die Antwort besteht aus einem Deckblatt (cover) und den Daten (data).

Das Deckblatt nennt den Exportzeitpunkt (exportedAt), die Art des Exports (exportType: SELF_SERVICE oder ADMIN), die betroffene Person (subject), die Rechtsgrundlage (legalBasis), Hinweise (notes) und je Kategorie Zählwert und Deckel-Kennzeichen (categories mit count und capped). Jede Quelle hat eine Obergrenze — 5.000 Einträge, bei Web-Benachrichtigungen 1.000 und bei Fehler-Telemetrie 500. Wird sie erreicht, weist das Deckblatt die Kategorie als gedeckelt aus: eine stille Kürzung wäre eine falsche „vollständige" Auskunft. Bricht eine Quelle, bricht der ganze Export mit einem Fehler — aus demselben Grund. Freitexte Dritter über die Person (etwa die Beschreibung eines Agenten) sind bewusst nicht enthalten und im Deckblatt benannt.

WegWerGrenze
GET /export/mejeder eingeloggte User, NUR die eigene ID1 Export / 24 h; ein fehlgeschlagener Lauf verbraucht das Kontingent nicht
GET /export/:userIdusers.dataExport (ADMIN/DSB)jede Person, kein 24h-Limit

Das Kontingent des Self-Service-Exports zählt je Person: Ein zweiter Aufruf innerhalb von 24 Stunden wird mit 429 und dem Fehlercode DATA_EXPORT_QUOTA_EXCEEDED abgelehnt; der Header Retry-After nennt die verbleibenden Sekunden, der Body trägt keine Wartezeit. Der Grund für die Grenze: Der Export ist teuer und wäre sonst ein bequemer Weg, Personendaten abzuziehen. Es gibt bewusst keinen Zurücksetzen-Aufruf — wer früher eine Auskunft braucht, bekommt sie über den Verwaltungs-Export (users.dataExport), der kein Kontingent kennt.

Sprache und Dateiname

Rechtsgrundlage und Hinweise des Deckblatts stehen in der Sprache der BETROFFENEN Person, nicht in der des Abrufers: zuerst ihre Profilsprache, ersatzweise die Systemsprache aus den allgemeinen Einstellungen, ersatzweise Englisch. Beim Verwaltungs-Export ist das bewusst eine andere Sprache als die Oberfläche des Abrufers — das Dokument ist eine Auskunft FÜR die betroffene Person. Ist in den allgemeinen Einstellungen keine Systemsprache gesetzt, erhalten Personen ohne Profilsprache das Deckblatt auf Englisch. Der Dateiname bleibt sprachneutral: data-export-<Datum>.json beim eigenen Export, data-export-<userId>-<Datum>.json beim Verwaltungs-Export.

Beide Wege sind auditiert (DATA_ACCESS) — mit Kategorien, Datensatzzahl und der betroffenen Person.

Anonymisierung (Art. 17)

Art. 17 wird als Anonymisierung umgesetzt. Der Grund: Ein vollständiges Löschen des Benutzers ließe alle Verweise auf ihn (Tickets, Kommentare, Historie) ins Leere laufen und zerstörte die fachliche Ticket-Historie. Deshalb bleibt der Datensatz als Skelett (Kennung, Zeitstempel, Status) erhalten, und sämtliche personenbezogenen Daten werden in einer Transaktion unwiderruflich überschrieben.

Schritt 1: Vorabprüfung — was blockiert?

Aktive Beteiligungen blockieren die Löschung, bis ein Admin sie auflöst. Ein Workflow-Schritt, dessen Bearbeiter verschwindet, würde sonst hängen; ein Approval-Quorum könnte unerreichbar werden. Art. 17 verlangt „unverzüglich" (praktisch ≤ 1 Monat) — das lässt Raum, aktive Vorgänge zuerst umzuhängen. GET /erasure-check liefert die Blocker mit Zählwerten und Entity-Links; POST /erase lehnt die Anonymisierung ab, solange Blocker bestehen.

GET /api/privacy/users/:id/erasure-check
{
  "erasable": false,
  "alreadyAnonymized": false,
  "isArchived": true,
  "erasureHold": false,
  "erasureHoldReason": null,
  "blockers": [
    {
      "type": "openTicketsAssigned",
      "count": 3,
      "refs": [{ "id": "tkt_1", "label": "T-1042", "url": "/tickets/tkt_1" }]
    },
    { "type": "pendingApprovals", "count": 1 }
  ]
}

erasable ist nur wahr, wenn kein Hindernis besteht, keine Lösch-Sperre gesetzt ist und die Person noch nicht anonymisiert wurde. Je Hindernis liefert die Prüfung bis zu fünf Referenzen mit Sprungziel — damit die Liste abgearbeitet und nicht nur gezählt werden kann; Typen ohne eigene Detailansicht tragen nur ein Label.

Die Blocker-Typen (müssen manuell aufgelöst werden): offene Tickets als Kunde oder Bearbeiter; offene Incidents/Problems/Changes/Change-Tasks als Bearbeiter; laufende Workflows als Initiator oder Schritt-Bearbeiter; PENDING-Approvals als Approver; PENDING-Handovers; zugewiesene Assets, gehaltene Verbrauchsartikel und aktive Lizenzen; alleiniges Mitglied einer sonst handlungsunfähigen Approval-Gruppe; Mailbox-CatchAll-Ziel; als fester Bearbeiter in einem Workflow-Template; als Ziel einer Eskalations-Policy, die sonst empfängerlos würde. Manches löst die Transaktion selbst auf (Vertretungen, Gruppen-Mitgliedschaften, Mailbox-Zugriffe, Manager-Beziehungen, User-ID-Listen wie geplante Report-Empfänger).

Schritt 2: Anonymisieren

Vorbedingungen (sonst wird der Aufruf mit einem eigenen Fehlercode abgelehnt, siehe Fehlercodes): kein geschütztes Konto (System-Benutzer, erstes Administrator-Konto), nicht der Aufrufer selbst, keine Lösch-Sperre, der User ist bereits ARCHIVIERT (Zwei-Schritt-Schutz; archivierte User können sich ohnehin nicht anmelden), noch nicht anonymisiert und frei von Hindernissen. Danach überschreibt EINE Transaktion die personenbezogenen Daten überall:

  • Stammdaten: E-Mail → erased+<id>@anonym.invalid, Name → „Gelöschter Benutzer", Avatar/Präferenzen/2FA-Secrets → null, anonymizedAt gesetzt
  • Name-Schnappschüsse in allen *Activity-/Timeline-Tabellen sowie an den Autoren von Ticket-Kommentaren (actorName → „Gelöschter Benutzer") — sonst stünde der Name dort weiter im Klartext
  • Attachment-Uploader/-IP/-UserAgent, Lizenz-Zuweisungs-E-Mails, Teilnehmer-E-Mails, Teams-Referenz, E-Mail-Absenderfelder
  • Abwesenheiten: reason/rejectedReason → null (Krankmeldungen + Freitext = gesundheitsnahe Daten, Art. 9)
  • Alle Zugänge des Kontos enden: Sessions/Tokens werden vollständig widerrufen (Refresh-Tokens + sid-Sessions), aktive WebSockets getrennt, Push-Anmeldungen der Geräte entfernt — und es erhält keine Benachrichtigungen mehr (siehe Notification-System)
  • Audit-Events des Akteurs: gezielter Content-Scrub (actor_name/IP/UserAgent), damit dort kein Klartext bis zu 7 Jahre überlebt — der actorId bleibt als Pseudonym, der Trail bleibt korrelierbar

Bewusste Grenze: Ticket- und Kommentar-TEXTE bleiben erhalten (Art. 17 Abs. 3 — Geschäftsvorgang); der Personenbezug fällt über die Anonymisierung von Fremdschlüsseln und Snapshots. Personenbezogene Daten IM Freitext sind ein manueller Redaktionsfall. Der Sync mit Microsoft Entra ID überspringt anonymisierte User (kein Re-Import). Anonymisierte Skelette sind zudem serverseitig aus allen User-Listen und Auswahl-Pickern (Bearbeiter, Kunde, Teilen, Checkout-Empfänger, Approver) ausgeblendet — nur die User-Verwaltung kann sie über einen Schalter einblenden.

Lösch-Sperre (Legal Hold)

POST /erasure-hold setzt eine absolute Sperre, die VOR allen anderen Prüfungen greift — auch vor der automatischen 180-Tage-Anonymisierung und vor dem Wiedereinspielen nach einer Sicherung. Solange die Sperre aktiv ist, ist der Lösch-Menüpunkt deaktiviert und jeder Lösch-Versuch schlägt kontrolliert fehl.

Der Aufruf ist je Richtung strikt: Das Setzen ({"enabled": true}) verlangt eine Begründung von 1 bis 500 Zeichen (z. B. laufendes Verfahren, Aufbewahrungspflicht) — eine dokumentationspflichtige Entscheidung soll ihren Grund tragen. Das Aufheben ({"enabled": false}) nimmt keine Begründung an. Jede andere Form des Aufrufs ist ein 400. Geschützte Konten haben keinen Lösch-Lebenszyklus und damit auch keine Lösch-Sperre; ein bereits anonymisiertes Konto nimmt keine Lösch-Sperre an. Beide Richtungen schreiben einen Audit-Eintrag (ERASURE_HOLD_SET bzw. ERASURE_HOLD_RELEASED), das Setzen mit der Begründung.

Fehlercodes

CodeHTTPBedeutung
ERASURE_BLOCKED400Aktive Beteiligungen bestehen; details.blockers nennt Typ und Anzahl je Hindernis
ERASURE_HOLD_ACTIVE400Lösch-Sperre gesetzt — erst aufheben, dann anonymisieren
ERASURE_REQUIRES_ARCHIVED400Das Konto muss vor der Anonymisierung archiviert sein
USER_ALREADY_ANONYMIZED400Bereits anonymisiert — der Vorgang ist irreversibel und nicht wiederholbar
VALIDATION_ERROR400Sperre ohne Begründung, Aufhebung mit Begründung, unbekannter Wert für kind oder hold
ERASURE_SELF_FORBIDDEN403Niemand anonymisiert das eigene Konto
PROTECTED_ACCOUNT403Geschütztes Konto (System-Benutzer, erstes Administrator-Konto): weder Anonymisierung noch Lösch-Sperre
DATA_EXPORT_QUOTA_EXCEEDED429Zweiter Self-Service-Export binnen 24 h; Retry-After nennt die Restzeit in Sekunden
NOT_FOUND404Die angefragte Person existiert nicht

Fristen-Modell

FallFristMechanik
Löschantrag (Art. 17)sofort, sobald blockerfreimanuell durch Admin/DSB
Ausgeschiedene (archivierte) User ohne Antrag180 TageTarget archivedUsers im retention_purge: Auto-Anonymisierung, wenn blockerfrei und ohne Hold; sonst übersprungen und im Job-Ergebnis gemeldet. Die Frist beginnt beim LETZTEN KONTAKT — gerechnet wird ab dem späteren von beidem: Archivierung oder letzte E-Mail zu diesem Konto, in beide Richtungen (die Antwort des Agenten zählt wie die Mail des Kunden). Ein archiviertes Konto, das weiter korrespondiert, wird nicht anonymisiert; ohne weiteren Kontakt läuft die Frist normal ab. Übersprungene weist das Ergebnis des Laufs getrennt aus (Sperre, laufender Kontakt, Hindernis)
Auto-E-Mail-Kontakte ohne offene Vorgänge90 / 180 TageTarget emailContacts im retention_purge: ganz ohne fachlichen Anker (kein Ticket als Kunde, keine Übergabe, keine Ticket-Beteiligung) → Hard-Delete 90 Tage nach letztem Kontakt; mit Anker und nur geschlossenen Vorgängen → Auto-Archiv nach 180 Tagen Inaktivität, danach greift die reguläre Anonymisierung archivierter User (Target archivedUsers). Offene Tickets, zugewiesene Assets, gehaltene Verbrauchsartikel, aktive Lizenzen und offene Approvals halten beide Stufen an. Mit dem Auto-Archiv enden auch die Zugänge des Kontos (Sitzungen, Live-Verbindungen, Push) und seine Benachrichtigungen
Anonymisierte Skeletteunbegrenztbewusst: ein Datensatz ohne Personenbezug enthält keine personenbezogenen Daten

Auch externe Handover-Empfänger (Techniker/Partner mit Leihgerät, als E-Mail-Kontakt angelegt) laufen über diesen Weg: solange Assets zugewiesen sind, greift die Blocker-Liste; nach Rückgabe archivieren und über die 180-Tage-Automatik anonymisieren. Handover-Protokolle bleiben als on-demand-PDF verfügbar und rendern nach der Anonymisierung den Pseudo-Namen (Hinweis „Empfänger anonymisiert").

Lösch-Rückstand

Der Rückstand beantwortet eine Frage: Wer müsste nach dem Fristen-Modell längst anonymisiert oder aufgeräumt sein, ist es aber nicht — und warum? Er enthält zwei Arten (Feld kind): überfällige archivierte Konten (ARCHIVED_OVERDUE) und inaktive, automatisch angelegte E-Mail-Kontakte (EMAIL_CONTACT_STALE). Die Fälligkeit rechnet die Abfrage mit derselben Semantik wie der nächtliche Lauf und aus denselben, live konfigurierten Fristen, damit Liste, Seiten, Sortierung und Kennzahlen dieselben Zeilen sehen. In der Oberfläche steht die Ansicht unter „Berichte & Audit → Lösch-Rückstand".

ArtStichtag der FristFrist
ARCHIVED_OVERDUEder spätere Zeitpunkt aus Archivierung und letzter E-Mail zum KontoFrist für archivierte Konten (Default 180 Tage)
EMAIL_CONTACT_STALEder spätere Zeitpunkt aus letzter Änderung und letzter E-Maildie Frist der jeweiligen Stufe: mit fachlichem Anker (ein Ticket als Kunde, eine Übergabe oder eine Ticket-Beteiligung) die Archiv-Frist (Default 180 Tage), ohne Anker die Lösch-Frist (Default 90 Tage)

Die Liste kommt als { data, pagination } und ist nach Überfälligkeit sortiert (die am längsten überfällige Zeile zuerst). Gesucht wird mit q über Name und E-Mail, geblättert mit page und per (Default 25, maximal 100). Zwei Filter grenzen ein: kind (ARCHIVED_OVERDUE oder EMAIL_CONTACT_STALE) und hold (true oder false); jeder andere Wert ist ein 400 statt einer stillen Annahme. Je Zeile stehen userId, name, email, kind, der Stichtag (referenceDate), die Tage über der Frist (daysOverdue), der Sperrstatus mit Begründung (erasureHold, erasureHoldReason) und isArchived. Die Hindernisse einer Zeile holt die Oberfläche über die Vorabprüfung derselben Person — es gibt nur eine Quelle dafür.

Die Kennzahlen-Route /erasure-backlog/stats liefert { stats, retention }: stats zählt archivedOverdue, emailContactsStale, holds und total über den UNGEFILTERTEN Rückstand — so bleiben die Kacheln beim Filtern stabil, während die Liste ihre gefilterte Summe in pagination.total führt; mit genau einem Kachel-Filter sind beide Zahlen gleich. holds zählt nur Sperren innerhalb des Rückstands, nicht alle Sperren des Systems. retention nennt die drei Fristen, nach denen die Ansicht rechnet (archivedUserRetentionDays, emailContactHardDeleteDays, emailContactArchiveDays). Konten MIT Lösch-Sperre stehen bewusst in der Liste, obwohl der nächtliche Lauf sie überspringt — die Sperre sichtbar zu machen ist der halbe Sinn der Ansicht.

Retention & Derivate

EIN nächtlicher Job retention_purge (03:00 UTC) setzt alle zeitbasierten Aufbewahrungsfristen der Datenbank in einem Lauf durch — pro Ziel isoliert (ein kaputtes Ziel stoppt die übrigen nicht) und mit einem Selbst-Audit-Event je Lauf. Die Standard-Fristen:

ZielRegel (Default)
AuditEventzweistufig — Content-Purge nach Policy (90d/365d/7y), Row-Delete am 7-Jahres-Maximum (siehe Audit-Seite)
ActivityLog180 Tage
RoleAuditLog365 Tage
EmailMessageticketlose Waisen (REJECTED/Bounces) älter 90 Tage; rawHeaders + Retry-Kopie älter 30 Tage genullt
WebNotification / ClientError30 / 90 Tage
ReportExecutionPreviews 24 h, Ergebnis-Daten 30 Tage genullt; Export-Dateien (CUSTOM_REPORT-Attachments) älter 30 Tage soft-deleted
JobExecution / PushSubscriptionalte Records / inaktive Subscriptions
notificationDigestItemsverwaiste Digest-Puffer-Items (Digest nach dem Puffern deaktiviert), Default 30 Tage — regulär werden Items nach dem Versand sofort gelöscht
BulkUpdateJob90 Tage
kbArticlessoft-gelöschte KB-Artikel (Papierkorb) älter 90 Tage → endgültiger Purge inkl. Revisionen, Tag-Orphans und Anhänge (Cascade)
emailContactsAuto-E-Mail-Kontakte: ankerlos → Hard-Delete 90 Tage; nur geschlossene Vorgänge → Auto-Archiv 180 Tage (siehe Fristen-Tabelle)
WorkflowInstance / UserAbsenceoptional, Default AUS — für Betreiber mit strengerer Formulardaten-/Art.-9-Policy

Grenzregel: Die zeitbasierte Aufbewahrung von Datenbank-Einträgen läuft über retention_purge; alles, was Dateien oder Virenscans anfasst (physisches Löschen, Quarantäne), bleibt beim attachment_cleanup-Job.

Derivate nach der Anonymisierung

Jede Stelle, die Personendaten rendert oder speichert, ist einer Klasse zugeordnet — so ist nachvollziehbar, was ein Beleg nach der Anonymisierung zeigt:

KlasseVerhalten
A — Live-Render aus DBselbstheilend: rendert automatisch anonym (Detail-/Listenansichten, Handover-PDF on-demand, Global Search, Report-Ausführung)
B — Snapshots mit Retentionein Name kann bis zum Fristablauf auftauchen — akzeptiert bei Fenster ≤ 90 Tage (Web-Notifications, genullte Report-Ergebnisse)
C — unveränderliche Storesgezielter Content-Scrub bei Anonymisierung (AuditEvent)
D — Dateienpro Datei entschieden (Avatare gelöscht, User-Uploads bleiben mit anonymem Uploader, Report-Exporte 30-Tage-Retention)
E — extern/irreversibelnicht rückholbar — versendete E-Mails, Teams-/Webex-Nachrichten, zugestellte Push (Art.-17-Grenze, in der Datenschutzerklärung ausgewiesen)
F — Backupsein Restore kann Gelöschte wiederbeleben → Löschungen nach jedem Restore erneut anwenden (siehe unten)

Backup-Restore: Ein Restore aus einem Dump von vor einer Löschung macht die Person wieder sichtbar. Deshalb: vor dem Restore die Liste der anonymisierten Konten aus der aktuellen Datenbank sichern, nach dem Restore alle seither erfolgten Löschungen erneut anwenden — das ist wiederholbar, bereits anonymisierte Konten bleiben unberührt. Eine Lösch-Sperre und bestehende Hindernisse gelten dabei genauso wie beim manuellen Weg. Das Vorgehen steht im Betriebs-Runbook der Sicherungen.

Öffentliche Transparenz

Datenschutzerklärung und Impressum sind unter /privacy und /imprint auch OHNE Login erreichbar — nötig für die drei Personengruppen, die die Login-Seite nie sehen: Empfänger der Art.-14-Info-Mail an auto-erstellte Kontakte, externe Handover-Empfänger auf der öffentlichen Bestätigungsseite, und E-Mail-Footer-Links. Die Fristen in der Datenschutzerklärung sind LIVE: /api/public/privacy-config liefert die echten, aktuell konfigurierten retention_purge-Parameter, sodass eine Admin-Änderung ohne Deployment erscheint (statische Werte nur als Offline-Fallback).

Die Datenschutzerklärung weist die eingesetzten Drittdienste aus (Cloudflare Turnstile, Microsoft Entra ID/Graph, Teams, Webex, Web-Push, SMTP), die Telemetrie mit gekürzter IP und die als Schutzmaßnahme umgesetzte Art.-9-Maskierung bei Abwesenheiten (Krankheits-Freitext wird ohne Sonderrecht gestrippt). Eine optionale Art.-14-Info-Mail an auto-erstellte E-Mail-Kontakte lässt sich global über einen Notification-Typ-Schalter aktivieren (Default AUS).

Berechtigungen

PermissionBeschreibung
users.eraseVorabprüfung, Anonymisierung und Lösch-Sperre (Standard: ADMIN; kritische Aktion, das Recht wird bei jedem Aufruf direkt in der Datenbank geprüft und ein Entzug wirkt sofort)
users.dataExportFremd-Export für eine beliebige Person (ADMIN/DSB). Der Self-Service-Export braucht keine Permission, nur eine gültige Session.
users.eraseaudit.enterpriseViewLesen des Lösch-Rückstands samt Kennzahlen — Handelnde UND reine Prüfer (z. B. Datenschutz) sehen ihn; die Aktionen darin bleiben an users.erase gebunden.
Verwandte Dokumentation