gdpr audit implemented, email log, vollmachten, pdf delete cancel data privacy and vollmachten, removed message no id card in engergy car, and other contracts that are not telecom contracts, added insert counter for engery
This commit is contained in:
parent
89cf92eaf5
commit
f2876f877e
224
README.md
224
README.md
|
|
@ -20,6 +20,7 @@ Web-basiertes CRM-System für Kundenverwaltung mit Verträgen (Energie, Telekomm
|
|||
- **Email-Provisionierung**: Automatische E-Mail-Weiterleitung bei Plesk/cPanel/DirectAdmin
|
||||
- **Berechtigungssystem**: Admin, Mitarbeiter, Nur-Lesen, Kundenportal
|
||||
- **Verschlüsselte Zugangsdaten**: Portal-Passwörter AES-256-GCM verschlüsselt
|
||||
- **DSGVO-Compliance**: Audit-Logging, Einwilligungsverwaltung, Datenexport, Löschanfragen
|
||||
- **Developer-Tools**: Datenbank-Browser und interaktives ER-Diagramm
|
||||
|
||||
## Tech Stack
|
||||
|
|
@ -207,7 +208,7 @@ npm run build # Produktions-Build erstellen
|
|||
npm run preview # Build-Vorschau
|
||||
```
|
||||
|
||||
### Docker
|
||||
### Docker (Entwicklung)
|
||||
|
||||
```bash
|
||||
docker-compose up -d # Container starten
|
||||
|
|
@ -216,6 +217,40 @@ docker-compose down -v # Container stoppen + Daten löschen
|
|||
docker-compose logs -f # Logs anzeigen
|
||||
```
|
||||
|
||||
### Docker (Produktion)
|
||||
|
||||
Im `docker/` Verzeichnis liegt ein komplettes Produktions-Setup:
|
||||
|
||||
```bash
|
||||
cd docker
|
||||
|
||||
# Image bauen
|
||||
docker-compose build
|
||||
|
||||
# Container starten
|
||||
docker-compose up -d
|
||||
|
||||
# Logs anzeigen
|
||||
docker-compose logs -f app
|
||||
```
|
||||
|
||||
**Komponenten:**
|
||||
- **MariaDB 10.11**: Datenbank
|
||||
- **App**: Backend + Frontend in einem Container
|
||||
- **Caddy**: Reverse-Proxy mit automatischem SSL
|
||||
|
||||
**Umgebungsvariablen (`docker/.env`):**
|
||||
```env
|
||||
MYSQL_ROOT_PASSWORD=sicheres-root-passwort
|
||||
MYSQL_DATABASE=opencrm
|
||||
MYSQL_USER=opencrm
|
||||
MYSQL_PASSWORD=sicheres-passwort
|
||||
JWT_SECRET=sehr-langer-zufaelliger-string
|
||||
ENCRYPTION_KEY=64-zeichen-hex-string
|
||||
DOMAIN=crm.example.com
|
||||
RUN_SEED=true # Nur beim ersten Start
|
||||
```
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
|
|
@ -223,9 +258,20 @@ opencrm/
|
|||
├── backend/
|
||||
│ ├── src/
|
||||
│ │ ├── controllers/ # Request-Handler
|
||||
│ │ │ ├── auditLog.controller.ts # Audit-Log API
|
||||
│ │ │ └── gdpr.controller.ts # DSGVO API
|
||||
│ │ ├── middleware/ # Auth, Validation
|
||||
│ │ │ ├── audit.ts # Automatisches API-Logging
|
||||
│ │ │ └── auditContext.ts # Before/After Context
|
||||
│ │ ├── routes/ # API-Endpunkte
|
||||
│ │ │ ├── auditLog.routes.ts # Audit-Log Routes
|
||||
│ │ │ └── gdpr.routes.ts # DSGVO Routes
|
||||
│ │ ├── services/ # Business-Logik
|
||||
│ │ │ ├── audit.service.ts # Hash-Kette, Logging
|
||||
│ │ │ ├── consent.service.ts # Einwilligungen
|
||||
│ │ │ └── gdpr.service.ts # Export, Löschung
|
||||
│ │ ├── lib/
|
||||
│ │ │ └── prisma.ts # Prisma mit Audit-Middleware
|
||||
│ │ ├── types/ # TypeScript-Typen
|
||||
│ │ └── index.ts # Server-Einstiegspunkt
|
||||
│ ├── prisma/
|
||||
|
|
@ -235,6 +281,7 @@ opencrm/
|
|||
│ │ ├── bank-cards/ # Bankkarten-Dokumente
|
||||
│ │ ├── documents/ # Ausweis-Scans
|
||||
│ │ ├── invoices/ # Rechnungsdokumente (Strom/Gas)
|
||||
│ │ ├── gdpr/ # DSGVO-Löschnachweise
|
||||
│ │ ├── business-registrations/ # Gewerbeanmeldungen
|
||||
│ │ ├── commercial-registers/ # Handelsregisterauszüge
|
||||
│ │ ├── privacy-policies/ # Datenschutzerklärungen
|
||||
|
|
@ -246,23 +293,31 @@ opencrm/
|
|||
│ ├── src/
|
||||
│ │ ├── components/ # UI-Komponenten
|
||||
│ │ ├── pages/ # Seiten
|
||||
│ │ │ └── settings/
|
||||
│ │ │ ├── AuditLogs.tsx # Audit-Protokoll
|
||||
│ │ │ └── GDPRDashboard.tsx # DSGVO-Dashboard
|
||||
│ │ ├── hooks/ # Custom Hooks
|
||||
│ │ ├── services/ # API-Client
|
||||
│ │ ├── types/ # TypeScript-Typen
|
||||
│ │ └── App.tsx # Haupt-Komponente
|
||||
│ └── package.json
|
||||
├── docker-compose.yml # MariaDB-Container
|
||||
├── docker/ # Docker-Deployment
|
||||
│ ├── Dockerfile # Multi-Stage Build
|
||||
│ ├── docker-compose.yml # Produktion (MariaDB, App, Caddy)
|
||||
│ ├── Caddyfile # Reverse-Proxy mit SSL
|
||||
│ └── entrypoint.sh # Container-Startup
|
||||
├── docker-compose.yml # MariaDB-Container (Entwicklung)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
| Rolle | Kunden | Verträge | Benutzer | Plattformen | Developer |
|
||||
|-------|--------|----------|----------|-------------|-----------|
|
||||
| Admin | CRUD | CRUD | CRUD | CRUD | Optional |
|
||||
| Mitarbeiter | CRUD | CRUD | - | Lesen | - |
|
||||
| Mitarbeiter (Lesen) | Lesen | Lesen | - | Lesen | - |
|
||||
| Kunde | Eigene | Eigene | - | - | - |
|
||||
| Rolle | Kunden | Verträge | Benutzer | Plattformen | Audit/DSGVO | Developer |
|
||||
|-------|--------|----------|----------|-------------|-------------|-----------|
|
||||
| Admin | CRUD | CRUD | CRUD | CRUD | Vollzugriff | Optional |
|
||||
| Mitarbeiter | CRUD | CRUD | - | Lesen | Lesen | - |
|
||||
| Mitarbeiter (Lesen) | Lesen | Lesen | - | Lesen | - | - |
|
||||
| Kunde | Eigene | Eigene | - | - | - | - |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
@ -708,6 +763,159 @@ In der Kundendetailansicht werden Verträge als **Baumstruktur** mit Vorgänger-
|
|||
|
||||
> **Hinweis:** In der Hauptvertragsliste (`/contracts`) wird weiterhin die flache Ansicht ohne Baumstruktur verwendet.
|
||||
|
||||
## DSGVO-Compliance & Audit-Logging
|
||||
|
||||
Umfassendes Audit-Logging-System mit DSGVO-Compliance-Features.
|
||||
|
||||
### Audit-Protokoll
|
||||
|
||||
Automatische Protokollierung aller API-Zugriffe mit:
|
||||
|
||||
| Feld | Beschreibung |
|
||||
|------|--------------|
|
||||
| **Benutzer** | User-ID, E-Mail, Rolle |
|
||||
| **Aktion** | CREATE, READ, UPDATE, DELETE, EXPORT, ANONYMIZE, LOGIN, LOGOUT |
|
||||
| **Ressource** | Tabelle + ID + lesbare Bezeichnung |
|
||||
| **Kontext** | Endpoint, HTTP-Methode, IP-Adresse, User-Agent |
|
||||
| **Änderungen** | Vorher/Nachher-Werte (bei Updates) |
|
||||
| **Sensitivität** | LOW, MEDIUM, HIGH, CRITICAL |
|
||||
| **Integrität** | SHA-256 Hash-Kette für Manipulationsschutz |
|
||||
|
||||
#### Sensitivitätsstufen
|
||||
|
||||
| Stufe | Ressourcen |
|
||||
|-------|------------|
|
||||
| **LOW** | Einstellungen, Plattformen, Tarife |
|
||||
| **MEDIUM** | Verträge, Provider |
|
||||
| **HIGH** | Kundendaten, Benutzerdaten |
|
||||
| **CRITICAL** | Authentifizierung, Bankdaten, Ausweisdokumente |
|
||||
|
||||
#### Zugriff
|
||||
|
||||
- **Einstellungen** → **Audit-Protokoll**
|
||||
- Filter nach: Datum, Benutzer, Aktion, Ressource, Sensitivität
|
||||
- Detail-Ansicht mit Vorher/Nachher-Diff
|
||||
- Export als JSON
|
||||
- Integritätsprüfung (Hash-Kette verifizieren)
|
||||
|
||||
### DSGVO-Dashboard
|
||||
|
||||
Zentrale Verwaltung für DSGVO-Anfragen unter **Einstellungen** → **DSGVO-Dashboard**.
|
||||
|
||||
#### Dashboard-Statistiken
|
||||
|
||||
- Offene Löschanfragen
|
||||
- Abgeschlossene Löschungen (letzte 30 Tage)
|
||||
- Datenexporte (letzte 30 Tage)
|
||||
- Aktive Einwilligungen
|
||||
|
||||
#### Einwilligungsverwaltung (Consents)
|
||||
|
||||
| Consent-Typ | Beschreibung |
|
||||
|-------------|--------------|
|
||||
| **DATA_PROCESSING** | Grundlegende Datenverarbeitung |
|
||||
| **MARKETING_EMAIL** | E-Mail-Marketing |
|
||||
| **MARKETING_PHONE** | Telefonmarketing |
|
||||
| **DATA_SHARING_PARTNER** | Datenweitergabe an Partner |
|
||||
|
||||
Einwilligungen können pro Kunde im Tab "Einwilligungen" verwaltet werden.
|
||||
|
||||
#### Löschanfragen (Art. 17)
|
||||
|
||||
Workflow für DSGVO-Löschanfragen:
|
||||
|
||||
| Status | Beschreibung |
|
||||
|--------|--------------|
|
||||
| **PENDING** | Anfrage eingegangen |
|
||||
| **IN_PROGRESS** | Wird bearbeitet |
|
||||
| **COMPLETED** | Vollständig gelöscht/anonymisiert |
|
||||
| **PARTIALLY_COMPLETED** | Teildaten behalten (z.B. aktive Verträge) |
|
||||
| **REJECTED** | Abgelehnt mit Begründung |
|
||||
|
||||
**Anonymisierung statt Löschung:**
|
||||
- Kundendaten werden anonymisiert (nicht gelöscht)
|
||||
- Aktive Verträge werden beibehalten
|
||||
- PDF-Löschnachweis wird generiert
|
||||
|
||||
#### Datenexport (Art. 15)
|
||||
|
||||
Kunden können alle gespeicherten Daten als JSON exportieren:
|
||||
- Stammdaten
|
||||
- Adressen, Bankdaten, Ausweise
|
||||
- Verträge mit Details
|
||||
- Zähler und Ablesungen
|
||||
- Einwilligungen
|
||||
- Zugriffsprotokolle
|
||||
|
||||
### API-Endpunkte
|
||||
|
||||
```
|
||||
# Audit-Logs
|
||||
GET /api/audit-logs # Logs mit Filtern
|
||||
GET /api/audit-logs/:id # Einzelnes Log
|
||||
GET /api/audit-logs/customer/:id # Logs für Kunde
|
||||
GET /api/audit-logs/export # Export (JSON/CSV)
|
||||
POST /api/audit-logs/verify # Hash-Kette prüfen
|
||||
GET /api/audit-logs/retention-policies # Aufbewahrungsfristen
|
||||
PUT /api/audit-logs/retention-policies/:id
|
||||
POST /api/audit-logs/cleanup # Manuelle Bereinigung
|
||||
|
||||
# DSGVO
|
||||
GET /api/gdpr/dashboard # Dashboard-Statistiken
|
||||
GET /api/gdpr/customer/:id/export # Kundendaten-Export
|
||||
GET /api/gdpr/deletions # Löschanfragen
|
||||
POST /api/gdpr/deletions # Löschanfrage erstellen
|
||||
PUT /api/gdpr/deletions/:id/process # Löschanfrage bearbeiten
|
||||
GET /api/gdpr/customer/:id/consents # Einwilligungen abrufen
|
||||
PUT /api/gdpr/customer/:id/consents/:type # Einwilligung ändern
|
||||
GET /api/gdpr/consents/overview # Consent-Übersicht
|
||||
```
|
||||
|
||||
### Aufbewahrungsfristen
|
||||
|
||||
| Ressource | Frist | Rechtsgrundlage |
|
||||
|-----------|-------|-----------------|
|
||||
| Standard | 10 Jahre | AO §147, HGB §257 |
|
||||
| Authentifizierung | 2 Jahre | Sicherheit |
|
||||
| Kundendaten (HIGH) | 10 Jahre | Steuerrecht |
|
||||
| Verträge | 10 Jahre | Steuerrecht |
|
||||
| Allgemein (LOW) | 3 Jahre | Verjährung |
|
||||
|
||||
### Berechtigungen
|
||||
|
||||
| Aktion | Berechtigung |
|
||||
|--------|--------------|
|
||||
| Audit-Logs lesen | `audit:read` |
|
||||
| Audit-Logs exportieren | `audit:export` |
|
||||
| Audit-Administration | `audit:admin` |
|
||||
| DSGVO-Export | `gdpr:export` |
|
||||
| Löschanfrage erstellen | `gdpr:delete` |
|
||||
| DSGVO-Administration | `gdpr:admin` |
|
||||
|
||||
### Technische Details
|
||||
|
||||
#### Hash-Kette
|
||||
|
||||
Jeder Audit-Log-Eintrag enthält einen SHA-256-Hash über:
|
||||
- Alle Felder des Eintrags
|
||||
- Hash des vorherigen Eintrags
|
||||
|
||||
Dies ermöglicht die Erkennung von Manipulationen.
|
||||
|
||||
#### Sensitive Daten
|
||||
|
||||
Folgende Felder werden in Audit-Logs gefiltert:
|
||||
- `password`, `passwordHash`
|
||||
- `portalPasswordHash`, `portalPasswordEncrypted`
|
||||
- `emailPasswordEncrypted`, `internetPasswordEncrypted`
|
||||
- `sipPasswordEncrypted`, `pin`, `puk`, `apiKey`
|
||||
|
||||
#### Performance
|
||||
|
||||
- Logging erfolgt asynchron (`setImmediate`)
|
||||
- API-Response wird nicht blockiert
|
||||
- Before/After-Werte über Prisma Middleware
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"contract.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/contract.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAK5C,OAAO,EAAe,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAI7D,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCjF;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BhF;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAuCnF;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBpF;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtF;AAED,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUvF;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUlF;AAID,wBAAsB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/E;AAID,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAyC/E"}
|
||||
{"version":3,"file":"contract.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/contract.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAM5C,OAAO,EAAe,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAI7D,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAgDjF;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChF;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAuCnF;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBpF;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtF;AAED,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUvF;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUlF;AAID,wBAAsB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/E;AAID,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAyC/E"}
|
||||
|
|
@ -49,6 +49,7 @@ const client_1 = require("@prisma/client");
|
|||
const contractService = __importStar(require("../services/contract.service.js"));
|
||||
const contractCockpitService = __importStar(require("../services/contractCockpit.service.js"));
|
||||
const contractHistoryService = __importStar(require("../services/contractHistory.service.js"));
|
||||
const authorizationService = __importStar(require("../services/authorization.service.js"));
|
||||
const prisma = new client_1.PrismaClient();
|
||||
async function getContracts(req, res) {
|
||||
try {
|
||||
|
|
@ -59,11 +60,19 @@ async function getContracts(req, res) {
|
|||
res.json({ success: true, data: treeData });
|
||||
return;
|
||||
}
|
||||
// Für Kundenportal-Benutzer: nur eigene + vertretene Kunden-Verträge anzeigen
|
||||
// Für Kundenportal-Benutzer: nur eigene + vertretene Kunden MIT Vollmacht
|
||||
let customerIds;
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
// Eigene Customer-ID + alle vertretenen Kunden-IDs
|
||||
customerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
// Eigene Customer-ID immer
|
||||
customerIds = [req.user.customerId];
|
||||
// Vertretene Kunden nur wenn Vollmacht erteilt
|
||||
const representedIds = req.user.representedCustomerIds || [];
|
||||
for (const repCustId of representedIds) {
|
||||
const hasAuth = await authorizationService.hasAuthorization(repCustId, req.user.customerId);
|
||||
if (hasAuth) {
|
||||
customerIds.push(repCustId);
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await contractService.getAllContracts({
|
||||
customerId: customerId ? parseInt(customerId) : undefined,
|
||||
|
|
@ -97,9 +106,16 @@ async function getContract(req, res) {
|
|||
});
|
||||
return;
|
||||
}
|
||||
// Für Kundenportal-Benutzer: Zugriff nur auf eigene + vertretene Kunden-Verträge
|
||||
// Für Kundenportal-Benutzer: Zugriff nur auf eigene + vertretene Kunden MIT Vollmacht
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
const allowedCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
const allowedCustomerIds = [req.user.customerId];
|
||||
const representedIds = req.user.representedCustomerIds || [];
|
||||
for (const repCustId of representedIds) {
|
||||
const hasAuth = await authorizationService.hasAuthorization(repCustId, req.user.customerId);
|
||||
if (hasAuth) {
|
||||
allowedCustomerIds.push(repCustId);
|
||||
}
|
||||
}
|
||||
if (!allowedCustomerIds.includes(contract.customerId)) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,5 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { AuthRequest } from '../types/index.js';
|
||||
export declare function getCustomers(req: Request, res: Response): Promise<void>;
|
||||
export declare function getCustomer(req: Request, res: Response): Promise<void>;
|
||||
export declare function createCustomer(req: Request, res: Response): Promise<void>;
|
||||
|
|
@ -24,6 +25,9 @@ export declare function getMeterReadings(req: Request, res: Response): Promise<v
|
|||
export declare function addMeterReading(req: Request, res: Response): Promise<void>;
|
||||
export declare function updateMeterReading(req: Request, res: Response): Promise<void>;
|
||||
export declare function deleteMeterReading(req: Request, res: Response): Promise<void>;
|
||||
export declare function reportMeterReading(req: AuthRequest, res: Response): Promise<void>;
|
||||
export declare function getMyMeters(req: AuthRequest, res: Response): Promise<void>;
|
||||
export declare function markReadingTransferred(req: AuthRequest, res: Response): Promise<void>;
|
||||
export declare function getPortalSettings(req: Request, res: Response): Promise<void>;
|
||||
export declare function updatePortalSettings(req: Request, res: Response): Promise<void>;
|
||||
export declare function setPortalPassword(req: Request, res: Response): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"customer.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/customer.controller.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"customer.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/customer.controller.ts"],"names":[],"mappings":"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"}
|
||||
|
|
@ -58,6 +58,9 @@ exports.getMeterReadings = getMeterReadings;
|
|||
exports.addMeterReading = addMeterReading;
|
||||
exports.updateMeterReading = updateMeterReading;
|
||||
exports.deleteMeterReading = deleteMeterReading;
|
||||
exports.reportMeterReading = reportMeterReading;
|
||||
exports.getMyMeters = getMyMeters;
|
||||
exports.markReadingTransferred = markReadingTransferred;
|
||||
exports.getPortalSettings = getPortalSettings;
|
||||
exports.updatePortalSettings = updatePortalSettings;
|
||||
exports.setPortalPassword = setPortalPassword;
|
||||
|
|
@ -66,8 +69,10 @@ exports.getRepresentatives = getRepresentatives;
|
|||
exports.addRepresentative = addRepresentative;
|
||||
exports.removeRepresentative = removeRepresentative;
|
||||
exports.searchForRepresentative = searchForRepresentative;
|
||||
const client_1 = require("@prisma/client");
|
||||
const customerService = __importStar(require("../services/customer.service.js"));
|
||||
const authService = __importStar(require("../services/auth.service.js"));
|
||||
const prisma = new client_1.PrismaClient();
|
||||
// Customer CRUD
|
||||
async function getCustomers(req, res) {
|
||||
try {
|
||||
|
|
@ -380,6 +385,88 @@ async function deleteMeterReading(req, res) {
|
|||
});
|
||||
}
|
||||
}
|
||||
// ==================== PORTAL: ZÄHLERSTAND MELDEN ====================
|
||||
async function reportMeterReading(req, res) {
|
||||
try {
|
||||
const user = req.user;
|
||||
if (!user?.isCustomerPortal || !user?.customerId) {
|
||||
res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
|
||||
return;
|
||||
}
|
||||
const meterId = parseInt(req.params.meterId);
|
||||
const { value, readingDate, notes } = req.body;
|
||||
// Prüfe ob der Zähler zum Kunden gehört
|
||||
const meter = await prisma.meter.findUnique({
|
||||
where: { id: meterId },
|
||||
select: { customerId: true },
|
||||
});
|
||||
if (!meter || meter.customerId !== user.customerId) {
|
||||
res.status(403).json({ success: false, error: 'Kein Zugriff auf diesen Zähler' });
|
||||
return;
|
||||
}
|
||||
const reading = await prisma.meterReading.create({
|
||||
data: {
|
||||
meterId,
|
||||
value: parseFloat(value),
|
||||
readingDate: readingDate ? new Date(readingDate) : new Date(),
|
||||
notes,
|
||||
reportedBy: user.email,
|
||||
status: 'REPORTED',
|
||||
},
|
||||
});
|
||||
res.status(201).json({ success: true, data: reading });
|
||||
}
|
||||
catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Melden des Zählerstands',
|
||||
});
|
||||
}
|
||||
}
|
||||
async function getMyMeters(req, res) {
|
||||
try {
|
||||
const user = req.user;
|
||||
if (!user?.isCustomerPortal || !user?.customerId) {
|
||||
res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
|
||||
return;
|
||||
}
|
||||
const meters = await prisma.meter.findMany({
|
||||
where: { customerId: user.customerId, isActive: true },
|
||||
include: {
|
||||
readings: {
|
||||
orderBy: { readingDate: 'desc' },
|
||||
take: 5,
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
res.json({ success: true, data: meters });
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden der Zähler' });
|
||||
}
|
||||
}
|
||||
async function markReadingTransferred(req, res) {
|
||||
try {
|
||||
const meterId = parseInt(req.params.meterId);
|
||||
const readingId = parseInt(req.params.readingId);
|
||||
const reading = await prisma.meterReading.update({
|
||||
where: { id: readingId },
|
||||
data: {
|
||||
status: 'TRANSFERRED',
|
||||
transferredAt: new Date(),
|
||||
transferredBy: req.user?.email,
|
||||
},
|
||||
});
|
||||
res.json({ success: true, data: reading });
|
||||
}
|
||||
catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren',
|
||||
});
|
||||
}
|
||||
}
|
||||
// ==================== PORTAL SETTINGS ====================
|
||||
async function getPortalSettings(req, res) {
|
||||
try {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -30,14 +30,25 @@ const emailProvider_routes_js_1 = __importDefault(require("./routes/emailProvide
|
|||
const cachedEmail_routes_js_1 = __importDefault(require("./routes/cachedEmail.routes.js"));
|
||||
const invoice_routes_js_1 = __importDefault(require("./routes/invoice.routes.js"));
|
||||
const contractHistory_routes_js_1 = __importDefault(require("./routes/contractHistory.routes.js"));
|
||||
const auditLog_routes_js_1 = __importDefault(require("./routes/auditLog.routes.js"));
|
||||
const gdpr_routes_js_1 = __importDefault(require("./routes/gdpr.routes.js"));
|
||||
const consent_public_routes_js_1 = __importDefault(require("./routes/consent-public.routes.js"));
|
||||
const emailLog_routes_js_1 = __importDefault(require("./routes/emailLog.routes.js"));
|
||||
const auditContext_js_1 = require("./middleware/auditContext.js");
|
||||
const audit_js_1 = require("./middleware/audit.js");
|
||||
dotenv_1.default.config();
|
||||
const app = (0, express_1.default)();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
// Middleware
|
||||
app.use((0, cors_1.default)());
|
||||
app.use(express_1.default.json());
|
||||
// Audit-Logging Middleware (DSGVO-konform)
|
||||
app.use(auditContext_js_1.auditContextMiddleware);
|
||||
app.use(audit_js_1.auditMiddleware);
|
||||
// Statische Dateien für Uploads
|
||||
app.use('/api/uploads', express_1.default.static(path_1.default.join(process.cwd(), 'uploads')));
|
||||
// Öffentliche Routes (OHNE Authentifizierung)
|
||||
app.use('/api/public/consent', consent_public_routes_js_1.default);
|
||||
// Routes
|
||||
app.use('/api/auth', auth_routes_js_1.default);
|
||||
app.use('/api/customers', customer_routes_js_1.default);
|
||||
|
|
@ -62,6 +73,9 @@ app.use('/api/email-providers', emailProvider_routes_js_1.default);
|
|||
app.use('/api', cachedEmail_routes_js_1.default);
|
||||
app.use('/api/energy-details', invoice_routes_js_1.default);
|
||||
app.use('/api', contractHistory_routes_js_1.default);
|
||||
app.use('/api/audit-logs', auditLog_routes_js_1.default);
|
||||
app.use('/api/gdpr', gdpr_routes_js_1.default);
|
||||
app.use('/api/email-logs', emailLog_routes_js_1.default);
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGjD,OAAO,EAAE,WAAW,EAAc,MAAM,mBAAmB,CAAC;AAI5D,wBAAsB,YAAY,CAChC,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,CAAC,CAuDf;AAED,wBAAgB,iBAAiB,CAAC,GAAG,mBAAmB,EAAE,MAAM,EAAE,IACxD,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,KAAG,IAAI,CAuBnE;AAGD,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,IAAI,CA4BN"}
|
||||
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGjD,OAAO,EAAE,WAAW,EAAc,MAAM,mBAAmB,CAAC;AAI5D,wBAAsB,YAAY,CAChC,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,CAAC,CAuDf;AAED,wBAAgB,iBAAiB,CAAC,GAAG,mBAAmB,EAAE,MAAM,EAAE,IACxD,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,KAAG,IAAI,CAuBnE;AAGD,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,IAAI,CAiCN"}
|
||||
|
|
@ -88,9 +88,13 @@ function requireCustomerAccess(req, res, next) {
|
|||
next();
|
||||
return;
|
||||
}
|
||||
// Customers can only access their own data
|
||||
// Customers can only access their own data + represented customers
|
||||
const customerId = parseInt(req.params.customerId || req.params.id);
|
||||
if (req.user.customerId && req.user.customerId === customerId) {
|
||||
const allowedIds = [
|
||||
req.user.customerId,
|
||||
...(req.user.representedCustomerIds || []),
|
||||
].filter(Boolean);
|
||||
if (allowedIds.includes(customerId)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"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"}
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"meter.routes.d.ts","sourceRoot":"","sources":["../../src/routes/meter.routes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAWxB,eAAe,MAAM,CAAC"}
|
||||
{"version":3,"file":"meter.routes.d.ts","sourceRoot":"","sources":["../../src/routes/meter.routes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAoBxB,eAAe,MAAM,CAAC"}
|
||||
|
|
@ -44,5 +44,11 @@ router.get('/:meterId/readings', auth_js_1.authenticate, customerController.getM
|
|||
router.post('/:meterId/readings', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('customers:update'), customerController.addMeterReading);
|
||||
router.put('/:meterId/readings/:readingId', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('customers:update'), customerController.updateMeterReading);
|
||||
router.delete('/:meterId/readings/:readingId', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('customers:delete'), customerController.deleteMeterReading);
|
||||
// Status-Update (Zählerstand als übertragen markieren)
|
||||
router.patch('/:meterId/readings/:readingId/transfer', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('customers:update'), customerController.markReadingTransferred);
|
||||
// Portal: Zählerstand melden (Kunde)
|
||||
router.post('/:meterId/readings/report', auth_js_1.authenticate, customerController.reportMeterReading);
|
||||
// Portal: Eigene Zähler laden
|
||||
router.get('/my-meters', auth_js_1.authenticate, customerController.getMyMeters);
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=meter.routes.js.map
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"meter.routes.js","sourceRoot":"","sources":["../../src/routes/meter.routes.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qCAAiC;AACjC,0FAA4E;AAC5E,mDAA+F;AAE/F,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;AACxG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;AAE3G,iBAAiB;AACjB,MAAM,CAAC,GAAG,CAAC,oBAAoB,EAAE,sBAAY,EAAE,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;AACpF,MAAM,CAAC,IAAI,CAAC,oBAAoB,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;AAC3H,MAAM,CAAC,GAAG,CAAC,+BAA+B,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,kBAAkB,CAAC,CAAC;AACxI,MAAM,CAAC,MAAM,CAAC,+BAA+B,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,kBAAkB,CAAC,CAAC;AAE3I,kBAAe,MAAM,CAAC"}
|
||||
{"version":3,"file":"meter.routes.js","sourceRoot":"","sources":["../../src/routes/meter.routes.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qCAAiC;AACjC,0FAA4E;AAC5E,mDAA+F;AAE/F,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;AACxG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;AAE3G,iBAAiB;AACjB,MAAM,CAAC,GAAG,CAAC,oBAAoB,EAAE,sBAAY,EAAE,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;AACpF,MAAM,CAAC,IAAI,CAAC,oBAAoB,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;AAC3H,MAAM,CAAC,GAAG,CAAC,+BAA+B,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,kBAAkB,CAAC,CAAC;AACxI,MAAM,CAAC,MAAM,CAAC,+BAA+B,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,kBAAkB,CAAC,CAAC;AAE3I,uDAAuD;AACvD,MAAM,CAAC,KAAK,CAAC,wCAAwC,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,sBAAsB,CAAC,CAAC;AAEvJ,qCAAqC;AACrC,MAAM,CAAC,IAAI,CAAC,2BAA2B,EAAE,sBAAY,EAAE,kBAAkB,CAAC,kBAAkB,CAAC,CAAC;AAE9F,8BAA8B;AAC9B,MAAM,CAAC,GAAG,CAAC,YAAY,EAAE,sBAAY,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;AAEvE,kBAAe,MAAM,CAAC"}
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"appSetting.service.d.ts","sourceRoot":"","sources":["../../src/services/appSetting.service.ts"],"names":[],"mappings":"AAaA,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAWpE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAGlE;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM1E;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAWtE;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAazE"}
|
||||
{"version":3,"file":"appSetting.service.d.ts","sourceRoot":"","sources":["../../src/services/appSetting.service.ts"],"names":[],"mappings":"AAgBA,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAWpE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAGlE;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM1E;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAWtE;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAazE"}
|
||||
|
|
@ -14,6 +14,9 @@ const DEFAULT_SETTINGS = {
|
|||
deadlineCriticalDays: '14', // Rot: Kritisch
|
||||
deadlineWarningDays: '42', // Gelb: Warnung (6 Wochen)
|
||||
deadlineOkDays: '90', // Grün: OK (3 Monate)
|
||||
// Ausweis-Ablauf: Fristenschwellen (in Tagen)
|
||||
documentExpiryCriticalDays: '30', // Rot: Kritisch (Standard 30 Tage)
|
||||
documentExpiryWarningDays: '90', // Gelb: Warnung (Standard 90 Tage)
|
||||
};
|
||||
async function getSetting(key) {
|
||||
const setting = await prisma.appSetting.findUnique({
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"appSetting.service.js","sourceRoot":"","sources":["../../src/services/appSetting.service.ts"],"names":[],"mappings":";;AAaA,gCAWC;AAED,wCAGC;AAED,gCAMC;AAED,wCAWC;AAED,8CAaC;AAjED,2CAA8C;AAE9C,MAAM,MAAM,GAAG,IAAI,qBAAY,EAAE,CAAC;AAElC,mBAAmB;AACnB,MAAM,gBAAgB,GAA2B;IAC/C,6BAA6B,EAAE,OAAO;IACtC,gDAAgD;IAChD,oBAAoB,EAAE,IAAI,EAAO,gBAAgB;IACjD,mBAAmB,EAAE,IAAI,EAAQ,2BAA2B;IAC5D,cAAc,EAAE,IAAI,EAAa,sBAAsB;CACxD,CAAC;AAEK,KAAK,UAAU,UAAU,CAAC,GAAW;IAC1C,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC;QACjD,KAAK,EAAE,EAAE,GAAG,EAAE;KACf,CAAC,CAAC;IAEH,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,OAAO,CAAC,KAAK,CAAC;IACvB,CAAC;IAED,2BAA2B;IAC3B,OAAO,gBAAgB,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;AACvC,CAAC;AAEM,KAAK,UAAU,cAAc,CAAC,GAAW;IAC9C,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC;IACpC,OAAO,KAAK,KAAK,MAAM,CAAC;AAC1B,CAAC;AAEM,KAAK,UAAU,UAAU,CAAC,GAAW,EAAE,KAAa;IACzD,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;QAC7B,KAAK,EAAE,EAAE,GAAG,EAAE;QACd,MAAM,EAAE,EAAE,KAAK,EAAE;QACjB,MAAM,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE;KACvB,CAAC,CAAC;AACL,CAAC;AAEM,KAAK,UAAU,cAAc;IAClC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;IAEpD,wDAAwD;IACxD,MAAM,MAAM,GAAG,EAAE,GAAG,gBAAgB,EAAE,CAAC;IAEvC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC;IACtC,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAEM,KAAK,UAAU,iBAAiB;IACrC,qFAAqF;IACrF,MAAM,UAAU,GAAG,CAAC,+BAA+B,CAAC,CAAC;IACrD,MAAM,WAAW,GAAG,MAAM,cAAc,EAAE,CAAC;IAE3C,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC;YACvB,MAAM,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
||||
{"version":3,"file":"appSetting.service.js","sourceRoot":"","sources":["../../src/services/appSetting.service.ts"],"names":[],"mappings":";;AAgBA,gCAWC;AAED,wCAGC;AAED,gCAMC;AAED,wCAWC;AAED,8CAaC;AApED,2CAA8C;AAE9C,MAAM,MAAM,GAAG,IAAI,qBAAY,EAAE,CAAC;AAElC,mBAAmB;AACnB,MAAM,gBAAgB,GAA2B;IAC/C,6BAA6B,EAAE,OAAO;IACtC,gDAAgD;IAChD,oBAAoB,EAAE,IAAI,EAAO,gBAAgB;IACjD,mBAAmB,EAAE,IAAI,EAAQ,2BAA2B;IAC5D,cAAc,EAAE,IAAI,EAAa,sBAAsB;IACvD,8CAA8C;IAC9C,0BAA0B,EAAE,IAAI,EAAE,mCAAmC;IACrE,yBAAyB,EAAE,IAAI,EAAG,mCAAmC;CACtE,CAAC;AAEK,KAAK,UAAU,UAAU,CAAC,GAAW;IAC1C,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC;QACjD,KAAK,EAAE,EAAE,GAAG,EAAE;KACf,CAAC,CAAC;IAEH,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,OAAO,CAAC,KAAK,CAAC;IACvB,CAAC;IAED,2BAA2B;IAC3B,OAAO,gBAAgB,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;AACvC,CAAC;AAEM,KAAK,UAAU,cAAc,CAAC,GAAW;IAC9C,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC;IACpC,OAAO,KAAK,KAAK,MAAM,CAAC;AAC1B,CAAC;AAEM,KAAK,UAAU,UAAU,CAAC,GAAW,EAAE,KAAa;IACzD,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;QAC7B,KAAK,EAAE,EAAE,GAAG,EAAE;QACd,MAAM,EAAE,EAAE,KAAK,EAAE;QACjB,MAAM,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE;KACvB,CAAC,CAAC;AACL,CAAC;AAEM,KAAK,UAAU,cAAc;IAClC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;IAEpD,wDAAwD;IACxD,MAAM,MAAM,GAAG,EAAE,GAAG,gBAAgB,EAAE,CAAC;IAEvC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC;IACtC,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAEM,KAAK,UAAU,iBAAiB;IACrC,qFAAqF;IACrF,MAAM,UAAU,GAAG,CAAC,+BAA+B,CAAC,CAAC;IACrD,MAAM,WAAW,GAAG,MAAM,cAAc,EAAE,CAAC;IAE3C,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC;YACvB,MAAM,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
||||
|
|
@ -53,6 +53,9 @@ export declare function getUserById(id: number): Promise<{
|
|||
lastName: string;
|
||||
isActive: boolean;
|
||||
customerId: number | null;
|
||||
whatsappNumber: string | null;
|
||||
telegramUsername: string | null;
|
||||
signalNumber: string | null;
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
isCustomerPortal: boolean;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"auth.service.d.ts","sourceRoot":"","sources":["../../src/services/auth.service.ts"],"names":[],"mappings":"AASA,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;;;;;;;;;;GA+D1D;AAGD,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;GAwFlE;AAGD,wBAAsB,yBAAyB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,iBAiBnF;AAGD,wBAAsB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgB1F;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;;;;;;GA8BA;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;UA8C3C;AAGD,wBAAsB,qBAAqB,CAAC,UAAU,EAAE,MAAM;;;;;;;;;;;;;;;;;UA+C7D"}
|
||||
{"version":3,"file":"auth.service.d.ts","sourceRoot":"","sources":["../../src/services/auth.service.ts"],"names":[],"mappings":"AASA,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;;;;;;;;;;GA+D1D;AAGD,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;GAwFlE;AAGD,wBAAsB,yBAAyB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,iBAiBnF;AAGD,wBAAsB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgB1F;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;;;;;;GA8BA;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;UAiD3C;AAGD,wBAAsB,qBAAqB,CAAC,UAAU,EAAE,MAAM;;;;;;;;;;;;;;;;;UA+C7D"}
|
||||
|
|
@ -246,6 +246,9 @@ async function getUserById(id) {
|
|||
lastName: user.lastName,
|
||||
isActive: user.isActive,
|
||||
customerId: user.customerId,
|
||||
whatsappNumber: user.whatsappNumber,
|
||||
telegramUsername: user.telegramUsername,
|
||||
signalNumber: user.signalNumber,
|
||||
roles: user.roles.map((ur) => ur.role.name),
|
||||
permissions: Array.from(permissions),
|
||||
isCustomerPortal: false,
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -157,6 +157,7 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
customerNumber: string;
|
||||
consentHash: string | null;
|
||||
portalEmail: string | null;
|
||||
type: import(".prisma/client").$Enums.CustomerType;
|
||||
salutation: string | null;
|
||||
|
|
@ -235,6 +236,10 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
|
|||
readingDate: Date;
|
||||
value: number;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}[];
|
||||
} & {
|
||||
|
|
@ -424,6 +429,10 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
|
|||
readingDate: Date;
|
||||
value: number;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}[];
|
||||
} & {
|
||||
|
|
@ -742,6 +751,7 @@ export declare function createContract(data: ContractCreateData): Promise<{
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
customerNumber: string;
|
||||
consentHash: string | null;
|
||||
portalEmail: string | null;
|
||||
type: import(".prisma/client").$Enums.CustomerType;
|
||||
salutation: string | null;
|
||||
|
|
@ -934,6 +944,7 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
customerNumber: string;
|
||||
consentHash: string | null;
|
||||
portalEmail: string | null;
|
||||
type: import(".prisma/client").$Enums.CustomerType;
|
||||
salutation: string | null;
|
||||
|
|
@ -1012,6 +1023,10 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
|
|||
readingDate: Date;
|
||||
value: number;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}[];
|
||||
} & {
|
||||
|
|
@ -1201,6 +1216,10 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
|
|||
readingDate: Date;
|
||||
value: number;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}[];
|
||||
} & {
|
||||
|
|
@ -1466,6 +1485,7 @@ export declare function createFollowUpContract(previousContractId: number): Prom
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
customerNumber: string;
|
||||
consentHash: string | null;
|
||||
portalEmail: string | null;
|
||||
type: import(".prisma/client").$Enums.CustomerType;
|
||||
salutation: string | null;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -44,10 +44,50 @@ export interface CockpitSummary {
|
|||
openTasks: number;
|
||||
pendingContracts: number;
|
||||
reviewDue: number;
|
||||
missingConsents: number;
|
||||
};
|
||||
}
|
||||
export interface DocumentAlert {
|
||||
id: number;
|
||||
type: string;
|
||||
documentNumber: string;
|
||||
expiryDate: string;
|
||||
daysUntilExpiry: number;
|
||||
urgency: UrgencyLevel;
|
||||
customer: {
|
||||
id: number;
|
||||
customerNumber: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
export interface ReportedMeterReading {
|
||||
id: number;
|
||||
readingDate: string;
|
||||
value: number;
|
||||
unit: string;
|
||||
notes?: string;
|
||||
reportedBy?: string;
|
||||
createdAt: string;
|
||||
meter: {
|
||||
id: number;
|
||||
meterNumber: string;
|
||||
type: string;
|
||||
};
|
||||
customer: {
|
||||
id: number;
|
||||
customerNumber: string;
|
||||
name: string;
|
||||
};
|
||||
providerPortal?: {
|
||||
providerName: string;
|
||||
portalUrl: string;
|
||||
portalUsername?: string;
|
||||
};
|
||||
}
|
||||
export interface CockpitResult {
|
||||
contracts: CockpitContract[];
|
||||
documentAlerts: DocumentAlert[];
|
||||
reportedReadings: ReportedMeterReading[];
|
||||
summary: CockpitSummary;
|
||||
thresholds: {
|
||||
criticalDays: number;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"contractCockpit.service.d.ts","sourceRoot":"","sources":["../../src/services/contractCockpit.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,cAAc,EACpC,MAAM,gBAAgB,CAAC;AAMxB,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAAC;AAElE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,YAAY,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,cAAc,EAAE,MAAM,CAAC;QACvB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,MAAM,CAAC,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,cAAc,EAAE,YAAY,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE;QACV,qBAAqB,EAAE,MAAM,CAAC;QAC9B,cAAc,EAAE,MAAM,CAAC;QACvB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;QACxB,SAAS,EAAE,MAAM,CAAC;QAClB,gBAAgB,EAAE,MAAM,CAAC;QACzB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE;QACV,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAyED,wBAAsB,cAAc,IAAI,OAAO,CAAC,aAAa,CAAC,CAod7D"}
|
||||
{"version":3,"file":"contractCockpit.service.d.ts","sourceRoot":"","sources":["../../src/services/contractCockpit.service.ts"],"names":[],"mappings":"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"}
|
||||
|
|
@ -105,6 +105,8 @@ async function getCockpitData() {
|
|||
const criticalDays = parseInt(settings.deadlineCriticalDays) || 14;
|
||||
const warningDays = parseInt(settings.deadlineWarningDays) || 42;
|
||||
const okDays = parseInt(settings.deadlineOkDays) || 90;
|
||||
const docExpiryCriticalDays = parseInt(settings.documentExpiryCriticalDays) || 30;
|
||||
const docExpiryWarningDays = parseInt(settings.documentExpiryWarningDays) || 90;
|
||||
// Lade alle relevanten Verträge (inkl. CANCELLED/DEACTIVATED für Schlussrechnung-Check)
|
||||
const contracts = await prisma.contract.findMany({
|
||||
where: {
|
||||
|
|
@ -191,8 +193,36 @@ async function getCockpitData() {
|
|||
openTasks: 0,
|
||||
pendingContracts: 0,
|
||||
reviewDue: 0,
|
||||
missingConsents: 0,
|
||||
},
|
||||
};
|
||||
// Consent-Daten batch-laden für alle Kunden
|
||||
const allConsents = await prisma.customerConsent.findMany({
|
||||
where: { status: 'GRANTED' },
|
||||
select: { customerId: true, consentType: true },
|
||||
});
|
||||
// Map: customerId → Set<consentType>
|
||||
const grantedConsentsMap = new Map();
|
||||
for (const c of allConsents) {
|
||||
if (!grantedConsentsMap.has(c.customerId)) {
|
||||
grantedConsentsMap.set(c.customerId, new Set());
|
||||
}
|
||||
grantedConsentsMap.get(c.customerId).add(c.consentType);
|
||||
}
|
||||
// Widerrufene Consents laden
|
||||
const withdrawnConsents = await prisma.customerConsent.findMany({
|
||||
where: { status: 'WITHDRAWN' },
|
||||
select: { customerId: true, consentType: true },
|
||||
});
|
||||
const withdrawnConsentsMap = new Map();
|
||||
for (const c of withdrawnConsents) {
|
||||
if (!withdrawnConsentsMap.has(c.customerId)) {
|
||||
withdrawnConsentsMap.set(c.customerId, new Set());
|
||||
}
|
||||
withdrawnConsentsMap.get(c.customerId).add(c.consentType);
|
||||
}
|
||||
// Track welche Kunden bereits eine Consent-Warnung bekommen haben (nur einmal pro Kunde)
|
||||
const customerConsentWarned = new Set();
|
||||
for (const contract of contracts) {
|
||||
const issues = [];
|
||||
// SNOOZE-LOGIK: Prüfen ob Snooze aktiv ist (für Fristen-Unterdrückung)
|
||||
|
|
@ -349,16 +379,41 @@ async function getCockpitData() {
|
|||
});
|
||||
summary.byCategory.missingData++;
|
||||
}
|
||||
// 7b. KEIN AUSWEIS (für DSL, FIBER, CABLE, MOBILE ist dies ein kritisches Problem)
|
||||
if (!contract.identityDocumentId) {
|
||||
// 7b. KEIN AUSWEIS (nur für Telekommunikationsprodukte relevant)
|
||||
const requiresIdentityDocument = ['DSL', 'FIBER', 'CABLE', 'MOBILE'].includes(contract.type);
|
||||
if (requiresIdentityDocument && !contract.identityDocumentId) {
|
||||
issues.push({
|
||||
type: 'missing_identity_document',
|
||||
label: 'Ausweis fehlt',
|
||||
urgency: requiresBankAndId ? 'critical' : 'warning',
|
||||
urgency: 'critical',
|
||||
details: 'Kein Ausweisdokument verknüpft',
|
||||
});
|
||||
summary.byCategory.missingData++;
|
||||
}
|
||||
// 7c. AUSWEIS LÄUFT AB (nur aktive Ausweise prüfen)
|
||||
if (contract.identityDocument && contract.identityDocument.isActive && contract.identityDocument.expiryDate) {
|
||||
const expiryDate = new Date(contract.identityDocument.expiryDate);
|
||||
const today = new Date();
|
||||
const daysUntilExpiry = Math.ceil((expiryDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
||||
if (daysUntilExpiry < 0) {
|
||||
issues.push({
|
||||
type: 'identity_document_expired',
|
||||
label: 'Ausweis abgelaufen',
|
||||
urgency: 'critical',
|
||||
details: `Ausweis seit ${Math.abs(daysUntilExpiry)} Tagen abgelaufen (${expiryDate.toLocaleDateString('de-DE')})`,
|
||||
});
|
||||
summary.byCategory.missingData++;
|
||||
}
|
||||
else if (daysUntilExpiry <= docExpiryWarningDays) {
|
||||
issues.push({
|
||||
type: 'identity_document_expiring',
|
||||
label: 'Ausweis läuft ab',
|
||||
urgency: daysUntilExpiry <= docExpiryCriticalDays ? 'critical' : 'warning',
|
||||
details: `Ausweis läuft in ${daysUntilExpiry} Tagen ab (${expiryDate.toLocaleDateString('de-DE')})`,
|
||||
});
|
||||
summary.byCategory.cancellationDeadlines++;
|
||||
}
|
||||
}
|
||||
// 8. ENERGIE-SPEZIFISCH: KEIN ZÄHLER
|
||||
if (['ELECTRICITY', 'GAS'].includes(contract.type) && contract.energyDetails) {
|
||||
if (!contract.energyDetails.meterId) {
|
||||
|
|
@ -475,6 +530,35 @@ async function getCockpitData() {
|
|||
}
|
||||
}
|
||||
}
|
||||
// #14 - Consent-Prüfung (nur für aktive Verträge, einmal pro Kunde)
|
||||
if (['ACTIVE', 'PENDING', 'DRAFT'].includes(contract.status) && !customerConsentWarned.has(contract.customer.id)) {
|
||||
const granted = grantedConsentsMap.get(contract.customer.id);
|
||||
const withdrawn = withdrawnConsentsMap.get(contract.customer.id);
|
||||
const requiredTypes = ['DATA_PROCESSING', 'MARKETING_EMAIL', 'MARKETING_PHONE', 'DATA_SHARING_PARTNER'];
|
||||
if (withdrawn && withdrawn.size > 0) {
|
||||
// Mindestens eine Einwilligung widerrufen
|
||||
issues.push({
|
||||
type: 'consent_withdrawn',
|
||||
label: 'Einwilligung widerrufen',
|
||||
urgency: 'critical',
|
||||
details: `${withdrawn.size} Einwilligung(en) widerrufen`,
|
||||
});
|
||||
summary.byCategory.missingConsents++;
|
||||
customerConsentWarned.add(contract.customer.id);
|
||||
}
|
||||
else if (!granted || granted.size < requiredTypes.length) {
|
||||
// Nicht alle 4 Einwilligungen erteilt
|
||||
const missing = requiredTypes.length - (granted?.size || 0);
|
||||
issues.push({
|
||||
type: 'missing_consents',
|
||||
label: 'Fehlende Einwilligungen',
|
||||
urgency: 'critical',
|
||||
details: `${missing} von ${requiredTypes.length} Einwilligungen fehlen`,
|
||||
});
|
||||
summary.byCategory.missingConsents++;
|
||||
customerConsentWarned.add(contract.customer.id);
|
||||
}
|
||||
}
|
||||
// Nur Verträge mit Issues hinzufügen
|
||||
if (issues.length > 0) {
|
||||
const highestUrgency = getHighestUrgency(issues);
|
||||
|
|
@ -523,8 +607,14 @@ async function getCockpitData() {
|
|||
};
|
||||
return urgencyOrder[a.highestUrgency] - urgencyOrder[b.highestUrgency];
|
||||
});
|
||||
// Vertragsunabhängige Ausweis-Warnungen
|
||||
const documentAlerts = await getDocumentExpiryAlerts(docExpiryCriticalDays, docExpiryWarningDays);
|
||||
// Gemeldete Zählerstände (REPORTED Status)
|
||||
const reportedReadings = await getReportedMeterReadings();
|
||||
return {
|
||||
contracts: cockpitContracts,
|
||||
documentAlerts,
|
||||
reportedReadings,
|
||||
summary,
|
||||
thresholds: {
|
||||
criticalDays,
|
||||
|
|
@ -533,4 +623,106 @@ async function getCockpitData() {
|
|||
},
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Alle aktiven Ausweise die ablaufen oder abgelaufen sind (vertragsunabhängig)
|
||||
*/
|
||||
async function getDocumentExpiryAlerts(criticalDays, warningDays) {
|
||||
const now = new Date();
|
||||
const inWarningDays = new Date(now.getTime() + warningDays * 24 * 60 * 60 * 1000);
|
||||
const documents = await prisma.identityDocument.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
expiryDate: { lte: inWarningDays },
|
||||
},
|
||||
include: {
|
||||
customer: {
|
||||
select: { id: true, customerNumber: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
orderBy: { expiryDate: 'asc' },
|
||||
});
|
||||
return documents.map((doc) => {
|
||||
const expiryDate = new Date(doc.expiryDate);
|
||||
const daysUntilExpiry = Math.ceil((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||
let urgency = 'warning';
|
||||
if (daysUntilExpiry < 0)
|
||||
urgency = 'critical';
|
||||
else if (daysUntilExpiry <= criticalDays)
|
||||
urgency = 'critical';
|
||||
return {
|
||||
id: doc.id,
|
||||
type: doc.type,
|
||||
documentNumber: doc.documentNumber,
|
||||
expiryDate: expiryDate.toISOString(),
|
||||
daysUntilExpiry,
|
||||
urgency,
|
||||
customer: {
|
||||
id: doc.customer.id,
|
||||
customerNumber: doc.customer.customerNumber,
|
||||
name: `${doc.customer.firstName} ${doc.customer.lastName}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Vom Kunden gemeldete Zählerstände die noch nicht übertragen wurden
|
||||
*/
|
||||
async function getReportedMeterReadings() {
|
||||
const readings = await prisma.meterReading.findMany({
|
||||
where: { status: 'REPORTED' },
|
||||
include: {
|
||||
meter: {
|
||||
include: {
|
||||
customer: {
|
||||
select: { id: true, customerNumber: true, firstName: true, lastName: true },
|
||||
},
|
||||
// Energie-Verträge für diesen Zähler (um Provider-Portal-Daten zu bekommen)
|
||||
energyDetails: {
|
||||
include: {
|
||||
contract: {
|
||||
select: {
|
||||
id: true,
|
||||
portalUsername: true,
|
||||
provider: {
|
||||
select: { id: true, name: true, portalUrl: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
return readings.map((r) => {
|
||||
const contract = r.meter.energyDetails?.[0]?.contract;
|
||||
const provider = contract?.provider;
|
||||
return {
|
||||
id: r.id,
|
||||
readingDate: r.readingDate.toISOString(),
|
||||
value: r.value,
|
||||
unit: r.unit,
|
||||
notes: r.notes ?? undefined,
|
||||
reportedBy: r.reportedBy ?? undefined,
|
||||
createdAt: r.createdAt.toISOString(),
|
||||
meter: {
|
||||
id: r.meter.id,
|
||||
meterNumber: r.meter.meterNumber,
|
||||
type: r.meter.type,
|
||||
},
|
||||
customer: {
|
||||
id: r.meter.customer.id,
|
||||
customerNumber: r.meter.customer.customerNumber,
|
||||
name: `${r.meter.customer.firstName} ${r.meter.customer.lastName}`,
|
||||
},
|
||||
providerPortal: provider?.portalUrl ? {
|
||||
providerName: provider.name,
|
||||
portalUrl: provider.portalUrl,
|
||||
portalUsername: contract?.portalUsername ?? undefined,
|
||||
} : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=contractCockpit.service.js.map
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -11,8 +11,8 @@ export declare function getTasksByContract(filters: ContractTaskFilters): Promis
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
title: string;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
completedAt: Date | null;
|
||||
taskId: number;
|
||||
}[];
|
||||
|
|
@ -23,9 +23,9 @@ export declare function getTasksByContract(filters: ContractTaskFilters): Promis
|
|||
description: string | null;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
contractId: number;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
visibleInPortal: boolean;
|
||||
createdBy: string | null;
|
||||
completedAt: Date | null;
|
||||
})[]>;
|
||||
export declare function getTaskById(id: number): Promise<{
|
||||
|
|
@ -35,9 +35,9 @@ export declare function getTaskById(id: number): Promise<{
|
|||
description: string | null;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
contractId: number;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
visibleInPortal: boolean;
|
||||
createdBy: string | null;
|
||||
completedAt: Date | null;
|
||||
} | null>;
|
||||
export declare function createTask(data: {
|
||||
|
|
@ -53,9 +53,9 @@ export declare function createTask(data: {
|
|||
description: string | null;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
contractId: number;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
visibleInPortal: boolean;
|
||||
createdBy: string | null;
|
||||
completedAt: Date | null;
|
||||
}>;
|
||||
export declare function updateTask(id: number, data: {
|
||||
|
|
@ -69,9 +69,9 @@ export declare function updateTask(id: number, data: {
|
|||
description: string | null;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
contractId: number;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
visibleInPortal: boolean;
|
||||
createdBy: string | null;
|
||||
completedAt: Date | null;
|
||||
}>;
|
||||
export declare function completeTask(id: number): Promise<{
|
||||
|
|
@ -81,9 +81,9 @@ export declare function completeTask(id: number): Promise<{
|
|||
description: string | null;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
contractId: number;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
visibleInPortal: boolean;
|
||||
createdBy: string | null;
|
||||
completedAt: Date | null;
|
||||
}>;
|
||||
export declare function reopenTask(id: number): Promise<{
|
||||
|
|
@ -93,9 +93,9 @@ export declare function reopenTask(id: number): Promise<{
|
|||
description: string | null;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
contractId: number;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
visibleInPortal: boolean;
|
||||
createdBy: string | null;
|
||||
completedAt: Date | null;
|
||||
}>;
|
||||
export declare function deleteTask(id: number): Promise<{
|
||||
|
|
@ -105,9 +105,9 @@ export declare function deleteTask(id: number): Promise<{
|
|||
description: string | null;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
contractId: number;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
visibleInPortal: boolean;
|
||||
createdBy: string | null;
|
||||
completedAt: Date | null;
|
||||
}>;
|
||||
export declare function createSubtask(data: {
|
||||
|
|
@ -119,8 +119,8 @@ export declare function createSubtask(data: {
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
title: string;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
completedAt: Date | null;
|
||||
taskId: number;
|
||||
}>;
|
||||
|
|
@ -131,8 +131,8 @@ export declare function updateSubtask(id: number, data: {
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
title: string;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
completedAt: Date | null;
|
||||
taskId: number;
|
||||
}>;
|
||||
|
|
@ -141,8 +141,8 @@ export declare function completeSubtask(id: number): Promise<{
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
title: string;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
completedAt: Date | null;
|
||||
taskId: number;
|
||||
}>;
|
||||
|
|
@ -151,8 +151,8 @@ export declare function reopenSubtask(id: number): Promise<{
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
title: string;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
completedAt: Date | null;
|
||||
taskId: number;
|
||||
}>;
|
||||
|
|
@ -161,8 +161,8 @@ export declare function deleteSubtask(id: number): Promise<{
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
title: string;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
completedAt: Date | null;
|
||||
taskId: number;
|
||||
}>;
|
||||
|
|
@ -174,9 +174,9 @@ export declare function getSubtaskById(id: number): Promise<({
|
|||
description: string | null;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
contractId: number;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
visibleInPortal: boolean;
|
||||
createdBy: string | null;
|
||||
completedAt: Date | null;
|
||||
};
|
||||
} & {
|
||||
|
|
@ -184,8 +184,8 @@ export declare function getSubtaskById(id: number): Promise<({
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
title: string;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
completedAt: Date | null;
|
||||
taskId: number;
|
||||
}) | null>;
|
||||
|
|
@ -223,8 +223,8 @@ export declare function getAllTasks(filters: AllTasksFilters): Promise<({
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
title: string;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
completedAt: Date | null;
|
||||
taskId: number;
|
||||
}[];
|
||||
|
|
@ -235,9 +235,9 @@ export declare function getAllTasks(filters: AllTasksFilters): Promise<({
|
|||
description: string | null;
|
||||
status: import(".prisma/client").$Enums.ContractTaskStatus;
|
||||
contractId: number;
|
||||
createdBy: string | null;
|
||||
title: string;
|
||||
visibleInPortal: boolean;
|
||||
createdBy: string | null;
|
||||
completedAt: Date | null;
|
||||
})[]>;
|
||||
export declare function getTaskStats(filters: AllTasksFilters): Promise<{
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export declare function getAllCustomers(filters: CustomerFilters): Promise<{
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
customerNumber: string;
|
||||
consentHash: string | null;
|
||||
portalEmail: string | null;
|
||||
type: import(".prisma/client").$Enums.CustomerType;
|
||||
salutation: string | null;
|
||||
|
|
@ -108,6 +109,10 @@ export declare function getCustomerById(id: number): Promise<({
|
|||
readingDate: Date;
|
||||
value: number;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}[];
|
||||
} & {
|
||||
|
|
@ -210,6 +215,7 @@ export declare function getCustomerById(id: number): Promise<({
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
customerNumber: string;
|
||||
consentHash: string | null;
|
||||
portalEmail: string | null;
|
||||
type: import(".prisma/client").$Enums.CustomerType;
|
||||
salutation: string | null;
|
||||
|
|
@ -257,6 +263,7 @@ export declare function createCustomer(data: {
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
customerNumber: string;
|
||||
consentHash: string | null;
|
||||
portalEmail: string | null;
|
||||
type: import(".prisma/client").$Enums.CustomerType;
|
||||
salutation: string | null;
|
||||
|
|
@ -300,6 +307,7 @@ export declare function updateCustomer(id: number, data: {
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
customerNumber: string;
|
||||
consentHash: string | null;
|
||||
portalEmail: string | null;
|
||||
type: import(".prisma/client").$Enums.CustomerType;
|
||||
salutation: string | null;
|
||||
|
|
@ -328,6 +336,7 @@ export declare function deleteCustomer(id: number): Promise<{
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
customerNumber: string;
|
||||
consentHash: string | null;
|
||||
portalEmail: string | null;
|
||||
type: import(".prisma/client").$Enums.CustomerType;
|
||||
salutation: string | null;
|
||||
|
|
@ -566,6 +575,10 @@ export declare function getCustomerMeters(customerId: number, showInactive?: boo
|
|||
readingDate: Date;
|
||||
value: number;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}[];
|
||||
} & {
|
||||
|
|
@ -629,6 +642,10 @@ export declare function addMeterReading(meterId: number, data: {
|
|||
readingDate: Date;
|
||||
value: number;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}>;
|
||||
export declare function getMeterReadings(meterId: number): Promise<{
|
||||
|
|
@ -638,6 +655,10 @@ export declare function getMeterReadings(meterId: number): Promise<{
|
|||
readingDate: Date;
|
||||
value: number;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}[]>;
|
||||
export declare function updateMeterReading(meterId: number, readingId: number, data: {
|
||||
|
|
@ -652,6 +673,10 @@ export declare function updateMeterReading(meterId: number, readingId: number, d
|
|||
readingDate: Date;
|
||||
value: number;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}>;
|
||||
export declare function deleteMeterReading(meterId: number, readingId: number): Promise<{
|
||||
|
|
@ -661,6 +686,10 @@ export declare function deleteMeterReading(meterId: number, readingId: number):
|
|||
readingDate: Date;
|
||||
value: number;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}>;
|
||||
export declare function updatePortalSettings(customerId: number, data: {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -20,6 +20,8 @@ export declare function getAllProviderConfigs(): Promise<{
|
|||
imapEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||
smtpEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||
allowSelfSignedCerts: boolean;
|
||||
systemEmailAddress: string | null;
|
||||
systemEmailPasswordEncrypted: string | null;
|
||||
}[]>;
|
||||
export declare function getProviderConfigById(id: number): Promise<{
|
||||
id: number;
|
||||
|
|
@ -42,6 +44,8 @@ export declare function getProviderConfigById(id: number): Promise<{
|
|||
imapEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||
smtpEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||
allowSelfSignedCerts: boolean;
|
||||
systemEmailAddress: string | null;
|
||||
systemEmailPasswordEncrypted: string | null;
|
||||
} | null>;
|
||||
export declare function getDefaultProviderConfig(): Promise<{
|
||||
id: number;
|
||||
|
|
@ -64,6 +68,8 @@ export declare function getDefaultProviderConfig(): Promise<{
|
|||
imapEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||
smtpEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||
allowSelfSignedCerts: boolean;
|
||||
systemEmailAddress: string | null;
|
||||
systemEmailPasswordEncrypted: string | null;
|
||||
} | null>;
|
||||
export declare function getActiveProviderConfig(): Promise<{
|
||||
id: number;
|
||||
|
|
@ -86,6 +92,8 @@ export declare function getActiveProviderConfig(): Promise<{
|
|||
imapEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||
smtpEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||
allowSelfSignedCerts: boolean;
|
||||
systemEmailAddress: string | null;
|
||||
systemEmailPasswordEncrypted: string | null;
|
||||
} | null>;
|
||||
export interface CreateProviderConfigData {
|
||||
name: string;
|
||||
|
|
@ -99,6 +107,8 @@ export interface CreateProviderConfigData {
|
|||
imapEncryption?: MailEncryption;
|
||||
smtpEncryption?: MailEncryption;
|
||||
allowSelfSignedCerts?: boolean;
|
||||
systemEmailAddress?: string;
|
||||
systemEmailPassword?: string;
|
||||
isActive?: boolean;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
|
@ -123,6 +133,8 @@ export declare function createProviderConfig(data: CreateProviderConfigData): Pr
|
|||
imapEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||
smtpEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||
allowSelfSignedCerts: boolean;
|
||||
systemEmailAddress: string | null;
|
||||
systemEmailPasswordEncrypted: string | null;
|
||||
}>;
|
||||
export declare function updateProviderConfig(id: number, data: Partial<CreateProviderConfigData>): Promise<{
|
||||
id: number;
|
||||
|
|
@ -145,6 +157,8 @@ export declare function updateProviderConfig(id: number, data: Partial<CreatePro
|
|||
imapEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||
smtpEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||
allowSelfSignedCerts: boolean;
|
||||
systemEmailAddress: string | null;
|
||||
systemEmailPasswordEncrypted: string | null;
|
||||
}>;
|
||||
export declare function deleteProviderConfig(id: number): Promise<{
|
||||
id: number;
|
||||
|
|
@ -167,6 +181,8 @@ export declare function deleteProviderConfig(id: number): Promise<{
|
|||
imapEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||
smtpEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||
allowSelfSignedCerts: boolean;
|
||||
systemEmailAddress: string | null;
|
||||
systemEmailPasswordEncrypted: string | null;
|
||||
}>;
|
||||
export declare function checkEmailExists(localPart: string): Promise<EmailExistsResult>;
|
||||
export declare function provisionEmail(localPart: string, customerEmail: string): Promise<EmailOperationResult>;
|
||||
|
|
@ -200,4 +216,17 @@ export declare function testProviderConnection(options?: {
|
|||
domain: string;
|
||||
};
|
||||
}): Promise<EmailOperationResult>;
|
||||
export interface SystemEmailCredentials {
|
||||
emailAddress: string;
|
||||
password: string;
|
||||
smtpServer: string;
|
||||
smtpPort: number;
|
||||
smtpEncryption: MailEncryption;
|
||||
allowSelfSignedCerts: boolean;
|
||||
}
|
||||
/**
|
||||
* System-E-Mail-Credentials vom aktiven Provider holen.
|
||||
* Wird für automatisierten Versand (DSGVO, Benachrichtigungen etc.) verwendet.
|
||||
*/
|
||||
export declare function getSystemEmailCredentials(): Promise<SystemEmailCredentials | null>;
|
||||
//# sourceMappingURL=emailProviderService.d.ts.map
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"emailProviderService.d.ts","sourceRoot":"","sources":["../../../src/services/emailProvider/emailProviderService.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"emailProviderService.d.ts","sourceRoot":"","sources":["../../../src/services/emailProvider/emailProviderService.ts"],"names":[],"mappings":"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"}
|
||||
|
|
@ -18,6 +18,7 @@ exports.deprovisionEmail = deprovisionEmail;
|
|||
exports.renameProvisionedEmail = renameProvisionedEmail;
|
||||
exports.getProviderDomain = getProviderDomain;
|
||||
exports.testProviderConnection = testProviderConnection;
|
||||
exports.getSystemEmailCredentials = getSystemEmailCredentials;
|
||||
const client_1 = require("@prisma/client");
|
||||
const encryption_js_1 = require("../../utils/encryption.js");
|
||||
const pleskProvider_js_1 = require("./pleskProvider.js");
|
||||
|
|
@ -70,9 +71,10 @@ async function createProviderConfig(data) {
|
|||
data: { isDefault: false },
|
||||
});
|
||||
}
|
||||
// Passwort verschlüsseln falls vorhanden
|
||||
// Passwörter verschlüsseln falls vorhanden
|
||||
const { encrypt } = await import('../../utils/encryption.js');
|
||||
const passwordEncrypted = data.password ? encrypt(data.password) : null;
|
||||
const systemEmailPasswordEncrypted = data.systemEmailPassword ? encrypt(data.systemEmailPassword) : null;
|
||||
return prisma.emailProviderConfig.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
|
|
@ -86,6 +88,8 @@ async function createProviderConfig(data) {
|
|||
imapEncryption: data.imapEncryption ?? 'SSL',
|
||||
smtpEncryption: data.smtpEncryption ?? 'SSL',
|
||||
allowSelfSignedCerts: data.allowSelfSignedCerts ?? false,
|
||||
systemEmailAddress: data.systemEmailAddress || null,
|
||||
systemEmailPasswordEncrypted,
|
||||
isActive: data.isActive ?? true,
|
||||
isDefault: data.isDefault ?? false,
|
||||
},
|
||||
|
|
@ -120,21 +124,31 @@ async function updateProviderConfig(id, data) {
|
|||
updateData.smtpEncryption = data.smtpEncryption;
|
||||
if (data.allowSelfSignedCerts !== undefined)
|
||||
updateData.allowSelfSignedCerts = data.allowSelfSignedCerts;
|
||||
if (data.systemEmailAddress !== undefined)
|
||||
updateData.systemEmailAddress = data.systemEmailAddress || null;
|
||||
if (data.isActive !== undefined)
|
||||
updateData.isActive = data.isActive;
|
||||
if (data.isDefault !== undefined)
|
||||
updateData.isDefault = data.isDefault;
|
||||
const { encrypt } = await import('../../utils/encryption.js');
|
||||
// Passwort-Logik:
|
||||
// - Wenn neues Passwort übergeben → verschlüsseln und speichern
|
||||
// - Wenn Benutzername gelöscht wird → Passwort auch löschen (gehören zusammen)
|
||||
if (data.password) {
|
||||
const { encrypt } = await import('../../utils/encryption.js');
|
||||
updateData.passwordEncrypted = encrypt(data.password);
|
||||
}
|
||||
else if (data.username !== undefined && !data.username) {
|
||||
// Benutzername wird gelöscht → Passwort auch löschen
|
||||
updateData.passwordEncrypted = null;
|
||||
}
|
||||
// System-E-Mail-Passwort
|
||||
if (data.systemEmailPassword) {
|
||||
updateData.systemEmailPasswordEncrypted = encrypt(data.systemEmailPassword);
|
||||
}
|
||||
else if (data.systemEmailAddress !== undefined && !data.systemEmailAddress) {
|
||||
// System-E-Mail wird gelöscht → Passwort auch löschen
|
||||
updateData.systemEmailPasswordEncrypted = null;
|
||||
}
|
||||
return prisma.emailProviderConfig.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
|
|
@ -477,4 +491,33 @@ async function testProviderConnection(options) {
|
|||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* System-E-Mail-Credentials vom aktiven Provider holen.
|
||||
* Wird für automatisierten Versand (DSGVO, Benachrichtigungen etc.) verwendet.
|
||||
*/
|
||||
async function getSystemEmailCredentials() {
|
||||
const config = await getActiveProviderConfig();
|
||||
if (!config?.systemEmailAddress || !config?.systemEmailPasswordEncrypted) {
|
||||
return null;
|
||||
}
|
||||
let password;
|
||||
try {
|
||||
password = (0, encryption_js_1.decrypt)(config.systemEmailPasswordEncrypted);
|
||||
}
|
||||
catch {
|
||||
console.error('System-E-Mail-Passwort konnte nicht entschlüsselt werden');
|
||||
return null;
|
||||
}
|
||||
const settings = await getImapSmtpSettings();
|
||||
if (!settings)
|
||||
return null;
|
||||
return {
|
||||
emailAddress: config.systemEmailAddress,
|
||||
password,
|
||||
smtpServer: settings.smtpServer,
|
||||
smtpPort: settings.smtpPort,
|
||||
smtpEncryption: settings.smtpEncryption,
|
||||
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=emailProviderService.js.map
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -20,12 +20,16 @@ export declare function getAllUsers(filters: UserFilters): Promise<{
|
|||
description: string | null;
|
||||
})[];
|
||||
hasDeveloperAccess: boolean;
|
||||
hasGdprAccess: boolean;
|
||||
id: number;
|
||||
email: string;
|
||||
customerId: number | null;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isActive: boolean;
|
||||
whatsappNumber: string | null;
|
||||
telegramUsername: string | null;
|
||||
signalNumber: string | null;
|
||||
createdAt: Date;
|
||||
}[];
|
||||
pagination: {
|
||||
|
|
@ -61,6 +65,9 @@ export declare function getUserById(id: number): Promise<{
|
|||
firstName: string;
|
||||
lastName: string;
|
||||
isActive: boolean;
|
||||
whatsappNumber: string | null;
|
||||
telegramUsername: string | null;
|
||||
signalNumber: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
} | null>;
|
||||
|
|
@ -72,6 +79,10 @@ export declare function createUser(data: {
|
|||
roleIds: number[];
|
||||
customerId?: number;
|
||||
hasDeveloperAccess?: boolean;
|
||||
hasGdprAccess?: boolean;
|
||||
whatsappNumber?: string;
|
||||
telegramUsername?: string;
|
||||
signalNumber?: string;
|
||||
}): Promise<{
|
||||
id: number;
|
||||
email: string;
|
||||
|
|
@ -101,6 +112,10 @@ export declare function updateUser(id: number, data: {
|
|||
roleIds?: number[];
|
||||
customerId?: number;
|
||||
hasDeveloperAccess?: boolean;
|
||||
hasGdprAccess?: boolean;
|
||||
whatsappNumber?: string;
|
||||
telegramUsername?: string;
|
||||
signalNumber?: string;
|
||||
}): Promise<{
|
||||
roles: ({
|
||||
permissions: ({
|
||||
|
|
@ -127,6 +142,9 @@ export declare function updateUser(id: number, data: {
|
|||
firstName: string;
|
||||
lastName: string;
|
||||
isActive: boolean;
|
||||
whatsappNumber: string | null;
|
||||
telegramUsername: string | null;
|
||||
signalNumber: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
} | null>;
|
||||
|
|
@ -139,6 +157,9 @@ export declare function deleteUser(id: number): Promise<{
|
|||
lastName: string;
|
||||
isActive: boolean;
|
||||
tokenInvalidatedAt: Date | null;
|
||||
whatsappNumber: string | null;
|
||||
telegramUsername: string | null;
|
||||
signalNumber: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}>;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"user.service.d.ts","sourceRoot":"","sources":["../../src/services/user.service.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqErD;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA0C3C;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;;;;;;;;;;;;;;;;;;;GAiCA;AAED,wBAAsB,UAAU,CAC9B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAwIF;AAoED,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;GA6D1C;AAGD,wBAAsB,WAAW;;;;;;;;;;;;;;;;;;;;MAYhC;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;WAS3C;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;;;;;;;;;;;;;;;;;GAeA;AAED,wBAAsB,UAAU,CAC9B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IACJ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;;;;;;;;;;;;;;;;;WAiBF;AAED,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM;;;;;;GAU1C;AAGD,wBAAsB,iBAAiB;;;;KAItC"}
|
||||
{"version":3,"file":"user.service.d.ts","sourceRoot":"","sources":["../../src/services/user.service.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4ErD;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA6C3C;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;;;;;;;;;;;;;;;;;;;GAyCA;AAED,wBAAsB,UAAU,CAC9B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA4IF;AA4GD,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;GA6D1C;AAGD,wBAAsB,WAAW;;;;;;;;;;;;;;;;;;;;MAYhC;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;WAS3C;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;;;;;;;;;;;;;;;;;GAeA;AAED,wBAAsB,UAAU,CAC9B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IACJ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;;;;;;;;;;;;;;;;;WAiBF;AAED,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM;;;;;;GAU1C;AAGD,wBAAsB,iBAAiB;;;;KAItC"}
|
||||
|
|
@ -48,6 +48,9 @@ async function getAllUsers(filters) {
|
|||
lastName: true,
|
||||
isActive: true,
|
||||
customerId: true,
|
||||
whatsappNumber: true,
|
||||
telegramUsername: true,
|
||||
signalNumber: true,
|
||||
createdAt: true,
|
||||
roles: {
|
||||
include: {
|
||||
|
|
@ -62,20 +65,24 @@ async function getAllUsers(filters) {
|
|||
}),
|
||||
prisma.user.count({ where }),
|
||||
]);
|
||||
// Get Developer role ID
|
||||
const developerRole = await prisma.role.findFirst({
|
||||
where: { name: 'Developer' },
|
||||
});
|
||||
// Get hidden role IDs
|
||||
const [developerRole, gdprRole] = await Promise.all([
|
||||
prisma.role.findFirst({ where: { name: 'Developer' } }),
|
||||
prisma.role.findFirst({ where: { name: 'DSGVO' } }),
|
||||
]);
|
||||
return {
|
||||
users: users.map((u) => {
|
||||
// Check if user has developer role assigned
|
||||
const hasDeveloperAccess = developerRole
|
||||
? u.roles.some((ur) => ur.roleId === developerRole.id)
|
||||
: false;
|
||||
const hasGdprAccess = gdprRole
|
||||
? u.roles.some((ur) => ur.roleId === gdprRole.id)
|
||||
: false;
|
||||
return {
|
||||
...u,
|
||||
roles: u.roles.map((r) => r.role),
|
||||
hasDeveloperAccess,
|
||||
hasGdprAccess,
|
||||
};
|
||||
}),
|
||||
pagination: (0, helpers_js_1.buildPaginationResponse)(page, limit, total),
|
||||
|
|
@ -91,6 +98,9 @@ async function getUserById(id) {
|
|||
lastName: true,
|
||||
isActive: true,
|
||||
customerId: true,
|
||||
whatsappNumber: true,
|
||||
telegramUsername: true,
|
||||
signalNumber: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
roles: {
|
||||
|
|
@ -129,6 +139,9 @@ async function createUser(data) {
|
|||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
customerId: data.customerId,
|
||||
whatsappNumber: data.whatsappNumber || null,
|
||||
telegramUsername: data.telegramUsername || null,
|
||||
signalNumber: data.signalNumber || null,
|
||||
roles: {
|
||||
create: data.roleIds.map((roleId) => ({ roleId })),
|
||||
},
|
||||
|
|
@ -149,10 +162,14 @@ async function createUser(data) {
|
|||
if (data.hasDeveloperAccess) {
|
||||
await setUserDeveloperAccess(user.id, true);
|
||||
}
|
||||
// DSGVO-Zugriff setzen falls aktiviert
|
||||
if (data.hasGdprAccess) {
|
||||
await setUserGdprAccess(user.id, true);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
async function updateUser(id, data) {
|
||||
const { roleIds, password, hasDeveloperAccess, ...userData } = data;
|
||||
const { roleIds, password, hasDeveloperAccess, hasGdprAccess, ...userData } = data;
|
||||
// Check if this would remove the last admin
|
||||
const isBeingDeactivated = userData.isActive === false;
|
||||
const rolesAreBeingChanged = roleIds !== undefined;
|
||||
|
|
@ -259,15 +276,17 @@ async function updateUser(id, data) {
|
|||
});
|
||||
}
|
||||
// Handle developer access
|
||||
console.log('updateUser - hasDeveloperAccess:', hasDeveloperAccess);
|
||||
if (hasDeveloperAccess !== undefined) {
|
||||
await setUserDeveloperAccess(id, hasDeveloperAccess);
|
||||
}
|
||||
// Handle GDPR access
|
||||
if (hasGdprAccess !== undefined) {
|
||||
await setUserGdprAccess(id, hasGdprAccess);
|
||||
}
|
||||
return getUserById(id);
|
||||
}
|
||||
// Helper to set developer access for a user
|
||||
async function setUserDeveloperAccess(userId, enabled) {
|
||||
console.log('setUserDeveloperAccess called - userId:', userId, 'enabled:', enabled);
|
||||
// Get or create developer:access permission
|
||||
let developerPerm = await prisma.permission.findFirst({
|
||||
where: { resource: 'developer', action: 'access' },
|
||||
|
|
@ -296,10 +315,7 @@ async function setUserDeveloperAccess(userId, enabled) {
|
|||
const hasRole = await prisma.userRole.findFirst({
|
||||
where: { userId, roleId: developerRole.id },
|
||||
});
|
||||
console.log('setUserDeveloperAccess - developerRole.id:', developerRole.id, 'hasRole:', hasRole);
|
||||
if (enabled && !hasRole) {
|
||||
// Add Developer role
|
||||
console.log('Adding Developer role');
|
||||
await prisma.userRole.create({
|
||||
data: { userId, roleId: developerRole.id },
|
||||
});
|
||||
|
|
@ -310,8 +326,6 @@ async function setUserDeveloperAccess(userId, enabled) {
|
|||
});
|
||||
}
|
||||
else if (!enabled && hasRole) {
|
||||
// Remove Developer role
|
||||
console.log('Removing Developer role');
|
||||
await prisma.userRole.delete({
|
||||
where: { userId_roleId: { userId, roleId: developerRole.id } },
|
||||
});
|
||||
|
|
@ -321,8 +335,51 @@ async function setUserDeveloperAccess(userId, enabled) {
|
|||
data: { tokenInvalidatedAt: new Date() },
|
||||
});
|
||||
}
|
||||
else {
|
||||
console.log('No action needed - enabled:', enabled, 'hasRole:', !!hasRole);
|
||||
}
|
||||
// Helper to set GDPR access for a user
|
||||
async function setUserGdprAccess(userId, enabled) {
|
||||
// Get or create DSGVO role
|
||||
let gdprRole = await prisma.role.findFirst({
|
||||
where: { name: 'DSGVO' },
|
||||
});
|
||||
if (!gdprRole) {
|
||||
// Create DSGVO role with all audit:* and gdpr:* permissions
|
||||
const gdprPermissions = await prisma.permission.findMany({
|
||||
where: {
|
||||
OR: [{ resource: 'audit' }, { resource: 'gdpr' }],
|
||||
},
|
||||
});
|
||||
gdprRole = await prisma.role.create({
|
||||
data: {
|
||||
name: 'DSGVO',
|
||||
description: 'DSGVO-Zugriff: Audit-Logs und Datenschutz-Verwaltung',
|
||||
permissions: {
|
||||
create: gdprPermissions.map((p) => ({ permissionId: p.id })),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
// Check if user already has DSGVO role
|
||||
const hasRole = await prisma.userRole.findFirst({
|
||||
where: { userId, roleId: gdprRole.id },
|
||||
});
|
||||
if (enabled && !hasRole) {
|
||||
await prisma.userRole.create({
|
||||
data: { userId, roleId: gdprRole.id },
|
||||
});
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { tokenInvalidatedAt: new Date() },
|
||||
});
|
||||
}
|
||||
else if (!enabled && hasRole) {
|
||||
await prisma.userRole.delete({
|
||||
where: { userId_roleId: { userId, roleId: gdprRole.id } },
|
||||
});
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { tokenInvalidatedAt: new Date() },
|
||||
});
|
||||
}
|
||||
}
|
||||
async function deleteUser(id) {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -122,6 +122,25 @@ exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
|||
Serializable: 'Serializable'
|
||||
});
|
||||
|
||||
exports.Prisma.EmailLogScalarFieldEnum = {
|
||||
id: 'id',
|
||||
fromAddress: 'fromAddress',
|
||||
toAddress: 'toAddress',
|
||||
subject: 'subject',
|
||||
context: 'context',
|
||||
customerId: 'customerId',
|
||||
triggeredBy: 'triggeredBy',
|
||||
smtpServer: 'smtpServer',
|
||||
smtpPort: 'smtpPort',
|
||||
smtpEncryption: 'smtpEncryption',
|
||||
smtpUser: 'smtpUser',
|
||||
success: 'success',
|
||||
messageId: 'messageId',
|
||||
errorMessage: 'errorMessage',
|
||||
smtpResponse: 'smtpResponse',
|
||||
sentAt: 'sentAt'
|
||||
};
|
||||
|
||||
exports.Prisma.AppSettingScalarFieldEnum = {
|
||||
id: 'id',
|
||||
key: 'key',
|
||||
|
|
@ -138,6 +157,9 @@ exports.Prisma.UserScalarFieldEnum = {
|
|||
lastName: 'lastName',
|
||||
isActive: 'isActive',
|
||||
tokenInvalidatedAt: 'tokenInvalidatedAt',
|
||||
whatsappNumber: 'whatsappNumber',
|
||||
telegramUsername: 'telegramUsername',
|
||||
signalNumber: 'signalNumber',
|
||||
customerId: 'customerId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
|
|
@ -186,6 +208,7 @@ exports.Prisma.CustomerScalarFieldEnum = {
|
|||
commercialRegisterPath: 'commercialRegisterPath',
|
||||
commercialRegisterNumber: 'commercialRegisterNumber',
|
||||
privacyPolicyPath: 'privacyPolicyPath',
|
||||
consentHash: 'consentHash',
|
||||
notes: 'notes',
|
||||
portalEnabled: 'portalEnabled',
|
||||
portalEmail: 'portalEmail',
|
||||
|
|
@ -206,6 +229,20 @@ exports.Prisma.CustomerRepresentativeScalarFieldEnum = {
|
|||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.RepresentativeAuthorizationScalarFieldEnum = {
|
||||
id: 'id',
|
||||
customerId: 'customerId',
|
||||
representativeId: 'representativeId',
|
||||
isGranted: 'isGranted',
|
||||
grantedAt: 'grantedAt',
|
||||
withdrawnAt: 'withdrawnAt',
|
||||
source: 'source',
|
||||
documentPath: 'documentPath',
|
||||
notes: 'notes',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.AddressScalarFieldEnum = {
|
||||
id: 'id',
|
||||
customerId: 'customerId',
|
||||
|
|
@ -267,6 +304,8 @@ exports.Prisma.EmailProviderConfigScalarFieldEnum = {
|
|||
imapEncryption: 'imapEncryption',
|
||||
smtpEncryption: 'smtpEncryption',
|
||||
allowSelfSignedCerts: 'allowSelfSignedCerts',
|
||||
systemEmailAddress: 'systemEmailAddress',
|
||||
systemEmailPasswordEncrypted: 'systemEmailPasswordEncrypted',
|
||||
isActive: 'isActive',
|
||||
isDefault: 'isDefault',
|
||||
createdAt: 'createdAt',
|
||||
|
|
@ -335,6 +374,10 @@ exports.Prisma.MeterReadingScalarFieldEnum = {
|
|||
value: 'value',
|
||||
unit: 'unit',
|
||||
notes: 'notes',
|
||||
reportedBy: 'reportedBy',
|
||||
status: 'status',
|
||||
transferredAt: 'transferredAt',
|
||||
transferredBy: 'transferredBy',
|
||||
createdAt: 'createdAt'
|
||||
};
|
||||
|
||||
|
|
@ -577,6 +620,80 @@ exports.Prisma.CarInsuranceDetailsScalarFieldEnum = {
|
|||
previousInsurer: 'previousInsurer'
|
||||
};
|
||||
|
||||
exports.Prisma.AuditLogScalarFieldEnum = {
|
||||
id: 'id',
|
||||
userId: 'userId',
|
||||
userEmail: 'userEmail',
|
||||
userRole: 'userRole',
|
||||
customerId: 'customerId',
|
||||
isCustomerPortal: 'isCustomerPortal',
|
||||
action: 'action',
|
||||
sensitivity: 'sensitivity',
|
||||
resourceType: 'resourceType',
|
||||
resourceId: 'resourceId',
|
||||
resourceLabel: 'resourceLabel',
|
||||
endpoint: 'endpoint',
|
||||
httpMethod: 'httpMethod',
|
||||
ipAddress: 'ipAddress',
|
||||
userAgent: 'userAgent',
|
||||
changesBefore: 'changesBefore',
|
||||
changesAfter: 'changesAfter',
|
||||
changesEncrypted: 'changesEncrypted',
|
||||
dataSubjectId: 'dataSubjectId',
|
||||
legalBasis: 'legalBasis',
|
||||
success: 'success',
|
||||
errorMessage: 'errorMessage',
|
||||
durationMs: 'durationMs',
|
||||
createdAt: 'createdAt',
|
||||
hash: 'hash',
|
||||
previousHash: 'previousHash'
|
||||
};
|
||||
|
||||
exports.Prisma.CustomerConsentScalarFieldEnum = {
|
||||
id: 'id',
|
||||
customerId: 'customerId',
|
||||
consentType: 'consentType',
|
||||
status: 'status',
|
||||
grantedAt: 'grantedAt',
|
||||
withdrawnAt: 'withdrawnAt',
|
||||
source: 'source',
|
||||
documentPath: 'documentPath',
|
||||
version: 'version',
|
||||
ipAddress: 'ipAddress',
|
||||
createdBy: 'createdBy',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.DataDeletionRequestScalarFieldEnum = {
|
||||
id: 'id',
|
||||
customerId: 'customerId',
|
||||
status: 'status',
|
||||
requestedAt: 'requestedAt',
|
||||
requestSource: 'requestSource',
|
||||
requestedBy: 'requestedBy',
|
||||
processedAt: 'processedAt',
|
||||
processedBy: 'processedBy',
|
||||
deletedData: 'deletedData',
|
||||
retainedData: 'retainedData',
|
||||
retentionReason: 'retentionReason',
|
||||
proofDocument: 'proofDocument',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.AuditRetentionPolicyScalarFieldEnum = {
|
||||
id: 'id',
|
||||
resourceType: 'resourceType',
|
||||
sensitivity: 'sensitivity',
|
||||
retentionDays: 'retentionDays',
|
||||
description: 'description',
|
||||
legalBasis: 'legalBasis',
|
||||
isActive: 'isActive',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
|
|
@ -625,6 +742,12 @@ exports.MeterType = exports.$Enums.MeterType = {
|
|||
GAS: 'GAS'
|
||||
};
|
||||
|
||||
exports.MeterReadingStatus = exports.$Enums.MeterReadingStatus = {
|
||||
RECORDED: 'RECORDED',
|
||||
REPORTED: 'REPORTED',
|
||||
TRANSFERRED: 'TRANSFERRED'
|
||||
};
|
||||
|
||||
exports.ContractType = exports.$Enums.ContractType = {
|
||||
ELECTRICITY: 'ELECTRICITY',
|
||||
GAS: 'GAS',
|
||||
|
|
@ -662,7 +785,48 @@ exports.InsuranceType = exports.$Enums.InsuranceType = {
|
|||
FULL: 'FULL'
|
||||
};
|
||||
|
||||
exports.AuditAction = exports.$Enums.AuditAction = {
|
||||
CREATE: 'CREATE',
|
||||
READ: 'READ',
|
||||
UPDATE: 'UPDATE',
|
||||
DELETE: 'DELETE',
|
||||
EXPORT: 'EXPORT',
|
||||
ANONYMIZE: 'ANONYMIZE',
|
||||
LOGIN: 'LOGIN',
|
||||
LOGOUT: 'LOGOUT',
|
||||
LOGIN_FAILED: 'LOGIN_FAILED'
|
||||
};
|
||||
|
||||
exports.AuditSensitivity = exports.$Enums.AuditSensitivity = {
|
||||
LOW: 'LOW',
|
||||
MEDIUM: 'MEDIUM',
|
||||
HIGH: 'HIGH',
|
||||
CRITICAL: 'CRITICAL'
|
||||
};
|
||||
|
||||
exports.ConsentType = exports.$Enums.ConsentType = {
|
||||
DATA_PROCESSING: 'DATA_PROCESSING',
|
||||
MARKETING_EMAIL: 'MARKETING_EMAIL',
|
||||
MARKETING_PHONE: 'MARKETING_PHONE',
|
||||
DATA_SHARING_PARTNER: 'DATA_SHARING_PARTNER'
|
||||
};
|
||||
|
||||
exports.ConsentStatus = exports.$Enums.ConsentStatus = {
|
||||
GRANTED: 'GRANTED',
|
||||
WITHDRAWN: 'WITHDRAWN',
|
||||
PENDING: 'PENDING'
|
||||
};
|
||||
|
||||
exports.DeletionRequestStatus = exports.$Enums.DeletionRequestStatus = {
|
||||
PENDING: 'PENDING',
|
||||
IN_PROGRESS: 'IN_PROGRESS',
|
||||
COMPLETED: 'COMPLETED',
|
||||
PARTIALLY_COMPLETED: 'PARTIALLY_COMPLETED',
|
||||
REJECTED: 'REJECTED'
|
||||
};
|
||||
|
||||
exports.Prisma.ModelName = {
|
||||
EmailLog: 'EmailLog',
|
||||
AppSetting: 'AppSetting',
|
||||
User: 'User',
|
||||
Role: 'Role',
|
||||
|
|
@ -671,6 +835,7 @@ exports.Prisma.ModelName = {
|
|||
UserRole: 'UserRole',
|
||||
Customer: 'Customer',
|
||||
CustomerRepresentative: 'CustomerRepresentative',
|
||||
RepresentativeAuthorization: 'RepresentativeAuthorization',
|
||||
Address: 'Address',
|
||||
BankCard: 'BankCard',
|
||||
IdentityDocument: 'IdentityDocument',
|
||||
|
|
@ -696,7 +861,11 @@ exports.Prisma.ModelName = {
|
|||
MobileContractDetails: 'MobileContractDetails',
|
||||
SimCard: 'SimCard',
|
||||
TvContractDetails: 'TvContractDetails',
|
||||
CarInsuranceDetails: 'CarInsuranceDetails'
|
||||
CarInsuranceDetails: 'CarInsuranceDetails',
|
||||
AuditLog: 'AuditLog',
|
||||
CustomerConsent: 'CustomerConsent',
|
||||
DataDeletionRequest: 'DataDeletionRequest',
|
||||
AuditRetentionPolicy: 'AuditRetentionPolicy'
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "prisma-client-652f85dbf9d7be282ff4b16714e4689fe4701aade21c76f6bcc5db624157e639",
|
||||
"name": "prisma-client-c6d54e22fa4d6137f643638da5d523e99ce84f9544cc793fd89163f1612953c6",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": "index-browser.js",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,36 @@ datasource db {
|
|||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ==================== EMAIL LOG ====================
|
||||
|
||||
model EmailLog {
|
||||
id Int @id @default(autoincrement())
|
||||
// Absender & Empfänger
|
||||
fromAddress String // Absender-E-Mail
|
||||
toAddress String // Empfänger-E-Mail
|
||||
subject String // Betreff
|
||||
// Versand-Kontext
|
||||
context String // z.B. "consent-link", "authorization-request", "customer-email"
|
||||
customerId Int? // Zugehöriger Kunde (falls vorhanden)
|
||||
triggeredBy String? // Wer hat den Versand ausgelöst (User-Email)
|
||||
// SMTP-Details
|
||||
smtpServer String // SMTP-Server
|
||||
smtpPort Int // SMTP-Port
|
||||
smtpEncryption String // SSL, STARTTLS, NONE
|
||||
smtpUser String // SMTP-Benutzername
|
||||
// Ergebnis
|
||||
success Boolean // Erfolgreich?
|
||||
messageId String? // Message-ID aus SMTP-Antwort
|
||||
errorMessage String? @db.Text // Fehlermeldung bei Fehler
|
||||
smtpResponse String? @db.Text // SMTP-Server-Antwort
|
||||
// Zeitstempel
|
||||
sentAt DateTime @default(now())
|
||||
|
||||
@@index([sentAt])
|
||||
@@index([customerId])
|
||||
@@index([success])
|
||||
}
|
||||
|
||||
// ==================== APP SETTINGS ====================
|
||||
|
||||
model AppSetting {
|
||||
|
|
@ -20,18 +50,24 @@ model AppSetting {
|
|||
// ==================== USERS & AUTH ====================
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
password String
|
||||
firstName String
|
||||
lastName String
|
||||
isActive Boolean @default(true)
|
||||
isActive Boolean @default(true)
|
||||
tokenInvalidatedAt DateTime? // Zeitpunkt ab dem alle Tokens ungültig sind (für Zwangslogout bei Rechteänderung)
|
||||
customerId Int? @unique
|
||||
customer Customer? @relation(fields: [customerId], references: [id])
|
||||
roles UserRole[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Messaging-Kanäle (für Datenschutz-Link-Versand)
|
||||
whatsappNumber String?
|
||||
telegramUsername String?
|
||||
signalNumber String?
|
||||
|
||||
customerId Int? @unique
|
||||
customer Customer? @relation(fields: [customerId], references: [id])
|
||||
roles UserRole[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Role {
|
||||
|
|
@ -97,6 +133,7 @@ model Customer {
|
|||
commercialRegisterPath String? // PDF-Pfad zum Handelsregisterauszug
|
||||
commercialRegisterNumber String? // Handelsregisternummer (Text)
|
||||
privacyPolicyPath String? // PDF-Pfad zur Datenschutzerklärung (für alle Kunden)
|
||||
consentHash String? @unique // Permanenter Hash für öffentlichen Einwilligungslink /datenschutz/<hash>
|
||||
notes String? @db.Text
|
||||
|
||||
// ===== Portal-Zugangsdaten =====
|
||||
|
|
@ -118,6 +155,13 @@ model Customer {
|
|||
representingFor CustomerRepresentative[] @relation("RepresentativeCustomer")
|
||||
representedBy CustomerRepresentative[] @relation("RepresentedCustomer")
|
||||
|
||||
// Vollmachten
|
||||
authorizationsGiven RepresentativeAuthorization[] @relation("AuthorizationCustomer")
|
||||
authorizationsReceived RepresentativeAuthorization[] @relation("AuthorizationRepresentative")
|
||||
|
||||
// DSGVO: Einwilligungen
|
||||
consents CustomerConsent[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
|
@ -140,6 +184,28 @@ model CustomerRepresentative {
|
|||
@@unique([customerId, representativeId]) // Keine doppelten Einträge
|
||||
}
|
||||
|
||||
// ==================== VOLLMACHTEN ====================
|
||||
// Vollmacht: Kunde B erteilt Kunde A die Vollmacht, seine Daten einzusehen
|
||||
// Ohne Vollmacht kann der Vertreter die Verträge des Kunden NICHT sehen
|
||||
|
||||
model RepresentativeAuthorization {
|
||||
id Int @id @default(autoincrement())
|
||||
customerId Int // Der Kunde, der die Vollmacht erteilt (z.B. Mutter)
|
||||
customer Customer @relation("AuthorizationCustomer", fields: [customerId], references: [id], onDelete: Cascade)
|
||||
representativeId Int // Der Vertreter, der Zugriff bekommt (z.B. Sohn)
|
||||
representative Customer @relation("AuthorizationRepresentative", fields: [representativeId], references: [id], onDelete: Cascade)
|
||||
isGranted Boolean @default(false) // Vollmacht erteilt?
|
||||
grantedAt DateTime? // Wann erteilt
|
||||
withdrawnAt DateTime? // Wann widerrufen
|
||||
source String? // Quelle: 'portal', 'papier', 'crm-backend'
|
||||
documentPath String? // PDF-Upload der unterschriebenen Vollmacht
|
||||
notes String? @db.Text // Notizen
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([customerId, representativeId]) // Eine Vollmacht pro Paar
|
||||
}
|
||||
|
||||
// ==================== ADDRESSES ====================
|
||||
|
||||
enum AddressType {
|
||||
|
|
@ -247,6 +313,10 @@ model EmailProviderConfig {
|
|||
smtpEncryption MailEncryption @default(SSL) // SSL, STARTTLS oder NONE
|
||||
allowSelfSignedCerts Boolean @default(false) // Selbstsignierte Zertifikate erlauben
|
||||
|
||||
// System-E-Mail für automatisierte Nachrichten (z.B. DSGVO Consent-Links)
|
||||
systemEmailAddress String? // z.B. "info@stressfrei-wechseln.de"
|
||||
systemEmailPasswordEncrypted String? // Passwort (verschlüsselt)
|
||||
|
||||
isActive Boolean @default(true)
|
||||
isDefault Boolean @default(false) // Standard-Provider
|
||||
createdAt DateTime @default(now())
|
||||
|
|
@ -356,14 +426,25 @@ model Meter {
|
|||
}
|
||||
|
||||
model MeterReading {
|
||||
id Int @id @default(autoincrement())
|
||||
meterId Int
|
||||
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
|
||||
readingDate DateTime
|
||||
value Float
|
||||
unit String @default("kWh")
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
id Int @id @default(autoincrement())
|
||||
meterId Int
|
||||
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
|
||||
readingDate DateTime
|
||||
value Float
|
||||
unit String @default("kWh")
|
||||
notes String?
|
||||
// Meldung & Übertragung
|
||||
reportedBy String? // Wer hat gemeldet? (E-Mail des Portal-Kunden oder Mitarbeiter)
|
||||
status MeterReadingStatus @default(RECORDED)
|
||||
transferredAt DateTime? // Wann wurde der Stand an den Anbieter übertragen?
|
||||
transferredBy String? // Wer hat übertragen?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
enum MeterReadingStatus {
|
||||
RECORDED // Erfasst (vom Mitarbeiter)
|
||||
REPORTED // Vom Kunden gemeldet (Portal)
|
||||
TRANSFERRED // An Anbieter übertragen
|
||||
}
|
||||
|
||||
// ==================== SALES PLATFORMS ====================
|
||||
|
|
@ -759,3 +840,170 @@ model CarInsuranceDetails {
|
|||
policyNumber String?
|
||||
previousInsurer String?
|
||||
}
|
||||
|
||||
// ==================== AUDIT LOGGING (DSGVO) ====================
|
||||
|
||||
enum AuditAction {
|
||||
CREATE
|
||||
READ
|
||||
UPDATE
|
||||
DELETE
|
||||
EXPORT // DSGVO-Datenexport
|
||||
ANONYMIZE // Recht auf Vergessenwerden
|
||||
LOGIN
|
||||
LOGOUT
|
||||
LOGIN_FAILED
|
||||
}
|
||||
|
||||
enum AuditSensitivity {
|
||||
LOW // Einstellungen, Plattformen
|
||||
MEDIUM // Verträge, Tarife
|
||||
HIGH // Kundendaten, Bankdaten
|
||||
CRITICAL // Authentifizierung, Ausweisdokumente
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
// Wer
|
||||
userId Int? // Staff User (null bei Kundenportal/System)
|
||||
userEmail String
|
||||
userRole String? @db.Text // Rolle zum Zeitpunkt der Aktion
|
||||
customerId Int? // Bei Kundenportal-Zugriff
|
||||
isCustomerPortal Boolean @default(false)
|
||||
|
||||
// Was
|
||||
action AuditAction
|
||||
sensitivity AuditSensitivity @default(MEDIUM)
|
||||
|
||||
// Welche Ressource
|
||||
resourceType String // Prisma Model Name
|
||||
resourceId String? // ID des Datensatzes
|
||||
resourceLabel String? // Lesbare Bezeichnung
|
||||
|
||||
// Kontext
|
||||
endpoint String // API-Pfad
|
||||
httpMethod String // GET, POST, PUT, DELETE
|
||||
ipAddress String
|
||||
userAgent String? @db.Text
|
||||
|
||||
// Änderungen (JSON, bei sensiblen Daten verschlüsselt)
|
||||
changesBefore String? @db.LongText
|
||||
changesAfter String? @db.LongText
|
||||
changesEncrypted Boolean @default(false)
|
||||
|
||||
// DSGVO
|
||||
dataSubjectId Int? // Betroffene Person (für Reports)
|
||||
legalBasis String? // Rechtsgrundlage
|
||||
|
||||
// Status
|
||||
success Boolean @default(true)
|
||||
errorMessage String? @db.Text
|
||||
durationMs Int?
|
||||
|
||||
// Unveränderlichkeit (Hash-Kette)
|
||||
createdAt DateTime @default(now())
|
||||
hash String? // SHA-256 Hash des Eintrags
|
||||
previousHash String? // Hash des vorherigen Eintrags
|
||||
|
||||
@@index([userId])
|
||||
@@index([customerId])
|
||||
@@index([resourceType, resourceId])
|
||||
@@index([dataSubjectId])
|
||||
@@index([action])
|
||||
@@index([createdAt])
|
||||
@@index([sensitivity])
|
||||
}
|
||||
|
||||
// ==================== CONSENT MANAGEMENT (DSGVO) ====================
|
||||
|
||||
enum ConsentType {
|
||||
DATA_PROCESSING // Grundlegende Datenverarbeitung
|
||||
MARKETING_EMAIL // E-Mail-Marketing
|
||||
MARKETING_PHONE // Telefon-Marketing
|
||||
DATA_SHARING_PARTNER // Weitergabe an Partner
|
||||
}
|
||||
|
||||
enum ConsentStatus {
|
||||
GRANTED
|
||||
WITHDRAWN
|
||||
PENDING
|
||||
}
|
||||
|
||||
model CustomerConsent {
|
||||
id Int @id @default(autoincrement())
|
||||
customerId Int
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
|
||||
consentType ConsentType
|
||||
status ConsentStatus @default(PENDING)
|
||||
|
||||
grantedAt DateTime?
|
||||
withdrawnAt DateTime?
|
||||
source String? // "portal", "telefon", "papier", "email"
|
||||
documentPath String? // Unterschriebenes Dokument
|
||||
version String? // Version der Datenschutzerklärung
|
||||
ipAddress String?
|
||||
|
||||
createdBy String // User der die Einwilligung erfasst hat
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([customerId, consentType])
|
||||
@@index([customerId])
|
||||
@@index([consentType])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
// ==================== DATA DELETION REQUESTS (DSGVO) ====================
|
||||
|
||||
enum DeletionRequestStatus {
|
||||
PENDING // Anfrage eingegangen
|
||||
IN_PROGRESS // Wird bearbeitet
|
||||
COMPLETED // Abgeschlossen
|
||||
PARTIALLY_COMPLETED // Teildaten behalten (rechtliche Gründe)
|
||||
REJECTED // Abgelehnt
|
||||
}
|
||||
|
||||
model DataDeletionRequest {
|
||||
id Int @id @default(autoincrement())
|
||||
customerId Int
|
||||
|
||||
status DeletionRequestStatus @default(PENDING)
|
||||
requestedAt DateTime @default(now())
|
||||
requestSource String // "email", "portal", "brief"
|
||||
requestedBy String // Wer hat angefragt
|
||||
|
||||
processedAt DateTime?
|
||||
processedBy String? // Mitarbeiter der bearbeitet hat
|
||||
|
||||
deletedData String? @db.LongText // JSON: Was wurde gelöscht
|
||||
retainedData String? @db.LongText // JSON: Was wurde behalten + Grund
|
||||
retentionReason String? @db.Text // Begründung für Aufbewahrung
|
||||
|
||||
proofDocument String? // Pfad zum Löschnachweis-PDF
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([customerId])
|
||||
@@index([status])
|
||||
@@index([requestedAt])
|
||||
}
|
||||
|
||||
// ==================== AUDIT RETENTION POLICIES ====================
|
||||
|
||||
model AuditRetentionPolicy {
|
||||
id Int @id @default(autoincrement())
|
||||
resourceType String // "*" für Standard, oder spezifischer Model-Name
|
||||
sensitivity AuditSensitivity?
|
||||
retentionDays Int // Aufbewahrungsfrist in Tagen (z.B. 3650 = 10 Jahre)
|
||||
description String?
|
||||
legalBasis String? // Gesetzliche Grundlage (z.B. "AO §147", "HGB §257")
|
||||
isActive Boolean @default(true)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([resourceType, sensitivity])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,25 @@ exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
|||
Serializable: 'Serializable'
|
||||
});
|
||||
|
||||
exports.Prisma.EmailLogScalarFieldEnum = {
|
||||
id: 'id',
|
||||
fromAddress: 'fromAddress',
|
||||
toAddress: 'toAddress',
|
||||
subject: 'subject',
|
||||
context: 'context',
|
||||
customerId: 'customerId',
|
||||
triggeredBy: 'triggeredBy',
|
||||
smtpServer: 'smtpServer',
|
||||
smtpPort: 'smtpPort',
|
||||
smtpEncryption: 'smtpEncryption',
|
||||
smtpUser: 'smtpUser',
|
||||
success: 'success',
|
||||
messageId: 'messageId',
|
||||
errorMessage: 'errorMessage',
|
||||
smtpResponse: 'smtpResponse',
|
||||
sentAt: 'sentAt'
|
||||
};
|
||||
|
||||
exports.Prisma.AppSettingScalarFieldEnum = {
|
||||
id: 'id',
|
||||
key: 'key',
|
||||
|
|
@ -138,6 +157,9 @@ exports.Prisma.UserScalarFieldEnum = {
|
|||
lastName: 'lastName',
|
||||
isActive: 'isActive',
|
||||
tokenInvalidatedAt: 'tokenInvalidatedAt',
|
||||
whatsappNumber: 'whatsappNumber',
|
||||
telegramUsername: 'telegramUsername',
|
||||
signalNumber: 'signalNumber',
|
||||
customerId: 'customerId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
|
|
@ -186,6 +208,7 @@ exports.Prisma.CustomerScalarFieldEnum = {
|
|||
commercialRegisterPath: 'commercialRegisterPath',
|
||||
commercialRegisterNumber: 'commercialRegisterNumber',
|
||||
privacyPolicyPath: 'privacyPolicyPath',
|
||||
consentHash: 'consentHash',
|
||||
notes: 'notes',
|
||||
portalEnabled: 'portalEnabled',
|
||||
portalEmail: 'portalEmail',
|
||||
|
|
@ -206,6 +229,20 @@ exports.Prisma.CustomerRepresentativeScalarFieldEnum = {
|
|||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.RepresentativeAuthorizationScalarFieldEnum = {
|
||||
id: 'id',
|
||||
customerId: 'customerId',
|
||||
representativeId: 'representativeId',
|
||||
isGranted: 'isGranted',
|
||||
grantedAt: 'grantedAt',
|
||||
withdrawnAt: 'withdrawnAt',
|
||||
source: 'source',
|
||||
documentPath: 'documentPath',
|
||||
notes: 'notes',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.AddressScalarFieldEnum = {
|
||||
id: 'id',
|
||||
customerId: 'customerId',
|
||||
|
|
@ -267,6 +304,8 @@ exports.Prisma.EmailProviderConfigScalarFieldEnum = {
|
|||
imapEncryption: 'imapEncryption',
|
||||
smtpEncryption: 'smtpEncryption',
|
||||
allowSelfSignedCerts: 'allowSelfSignedCerts',
|
||||
systemEmailAddress: 'systemEmailAddress',
|
||||
systemEmailPasswordEncrypted: 'systemEmailPasswordEncrypted',
|
||||
isActive: 'isActive',
|
||||
isDefault: 'isDefault',
|
||||
createdAt: 'createdAt',
|
||||
|
|
@ -335,6 +374,10 @@ exports.Prisma.MeterReadingScalarFieldEnum = {
|
|||
value: 'value',
|
||||
unit: 'unit',
|
||||
notes: 'notes',
|
||||
reportedBy: 'reportedBy',
|
||||
status: 'status',
|
||||
transferredAt: 'transferredAt',
|
||||
transferredBy: 'transferredBy',
|
||||
createdAt: 'createdAt'
|
||||
};
|
||||
|
||||
|
|
@ -577,6 +620,80 @@ exports.Prisma.CarInsuranceDetailsScalarFieldEnum = {
|
|||
previousInsurer: 'previousInsurer'
|
||||
};
|
||||
|
||||
exports.Prisma.AuditLogScalarFieldEnum = {
|
||||
id: 'id',
|
||||
userId: 'userId',
|
||||
userEmail: 'userEmail',
|
||||
userRole: 'userRole',
|
||||
customerId: 'customerId',
|
||||
isCustomerPortal: 'isCustomerPortal',
|
||||
action: 'action',
|
||||
sensitivity: 'sensitivity',
|
||||
resourceType: 'resourceType',
|
||||
resourceId: 'resourceId',
|
||||
resourceLabel: 'resourceLabel',
|
||||
endpoint: 'endpoint',
|
||||
httpMethod: 'httpMethod',
|
||||
ipAddress: 'ipAddress',
|
||||
userAgent: 'userAgent',
|
||||
changesBefore: 'changesBefore',
|
||||
changesAfter: 'changesAfter',
|
||||
changesEncrypted: 'changesEncrypted',
|
||||
dataSubjectId: 'dataSubjectId',
|
||||
legalBasis: 'legalBasis',
|
||||
success: 'success',
|
||||
errorMessage: 'errorMessage',
|
||||
durationMs: 'durationMs',
|
||||
createdAt: 'createdAt',
|
||||
hash: 'hash',
|
||||
previousHash: 'previousHash'
|
||||
};
|
||||
|
||||
exports.Prisma.CustomerConsentScalarFieldEnum = {
|
||||
id: 'id',
|
||||
customerId: 'customerId',
|
||||
consentType: 'consentType',
|
||||
status: 'status',
|
||||
grantedAt: 'grantedAt',
|
||||
withdrawnAt: 'withdrawnAt',
|
||||
source: 'source',
|
||||
documentPath: 'documentPath',
|
||||
version: 'version',
|
||||
ipAddress: 'ipAddress',
|
||||
createdBy: 'createdBy',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.DataDeletionRequestScalarFieldEnum = {
|
||||
id: 'id',
|
||||
customerId: 'customerId',
|
||||
status: 'status',
|
||||
requestedAt: 'requestedAt',
|
||||
requestSource: 'requestSource',
|
||||
requestedBy: 'requestedBy',
|
||||
processedAt: 'processedAt',
|
||||
processedBy: 'processedBy',
|
||||
deletedData: 'deletedData',
|
||||
retainedData: 'retainedData',
|
||||
retentionReason: 'retentionReason',
|
||||
proofDocument: 'proofDocument',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.AuditRetentionPolicyScalarFieldEnum = {
|
||||
id: 'id',
|
||||
resourceType: 'resourceType',
|
||||
sensitivity: 'sensitivity',
|
||||
retentionDays: 'retentionDays',
|
||||
description: 'description',
|
||||
legalBasis: 'legalBasis',
|
||||
isActive: 'isActive',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
|
|
@ -625,6 +742,12 @@ exports.MeterType = exports.$Enums.MeterType = {
|
|||
GAS: 'GAS'
|
||||
};
|
||||
|
||||
exports.MeterReadingStatus = exports.$Enums.MeterReadingStatus = {
|
||||
RECORDED: 'RECORDED',
|
||||
REPORTED: 'REPORTED',
|
||||
TRANSFERRED: 'TRANSFERRED'
|
||||
};
|
||||
|
||||
exports.ContractType = exports.$Enums.ContractType = {
|
||||
ELECTRICITY: 'ELECTRICITY',
|
||||
GAS: 'GAS',
|
||||
|
|
@ -662,7 +785,48 @@ exports.InsuranceType = exports.$Enums.InsuranceType = {
|
|||
FULL: 'FULL'
|
||||
};
|
||||
|
||||
exports.AuditAction = exports.$Enums.AuditAction = {
|
||||
CREATE: 'CREATE',
|
||||
READ: 'READ',
|
||||
UPDATE: 'UPDATE',
|
||||
DELETE: 'DELETE',
|
||||
EXPORT: 'EXPORT',
|
||||
ANONYMIZE: 'ANONYMIZE',
|
||||
LOGIN: 'LOGIN',
|
||||
LOGOUT: 'LOGOUT',
|
||||
LOGIN_FAILED: 'LOGIN_FAILED'
|
||||
};
|
||||
|
||||
exports.AuditSensitivity = exports.$Enums.AuditSensitivity = {
|
||||
LOW: 'LOW',
|
||||
MEDIUM: 'MEDIUM',
|
||||
HIGH: 'HIGH',
|
||||
CRITICAL: 'CRITICAL'
|
||||
};
|
||||
|
||||
exports.ConsentType = exports.$Enums.ConsentType = {
|
||||
DATA_PROCESSING: 'DATA_PROCESSING',
|
||||
MARKETING_EMAIL: 'MARKETING_EMAIL',
|
||||
MARKETING_PHONE: 'MARKETING_PHONE',
|
||||
DATA_SHARING_PARTNER: 'DATA_SHARING_PARTNER'
|
||||
};
|
||||
|
||||
exports.ConsentStatus = exports.$Enums.ConsentStatus = {
|
||||
GRANTED: 'GRANTED',
|
||||
WITHDRAWN: 'WITHDRAWN',
|
||||
PENDING: 'PENDING'
|
||||
};
|
||||
|
||||
exports.DeletionRequestStatus = exports.$Enums.DeletionRequestStatus = {
|
||||
PENDING: 'PENDING',
|
||||
IN_PROGRESS: 'IN_PROGRESS',
|
||||
COMPLETED: 'COMPLETED',
|
||||
PARTIALLY_COMPLETED: 'PARTIALLY_COMPLETED',
|
||||
REJECTED: 'REJECTED'
|
||||
};
|
||||
|
||||
exports.Prisma.ModelName = {
|
||||
EmailLog: 'EmailLog',
|
||||
AppSetting: 'AppSetting',
|
||||
User: 'User',
|
||||
Role: 'Role',
|
||||
|
|
@ -671,6 +835,7 @@ exports.Prisma.ModelName = {
|
|||
UserRole: 'UserRole',
|
||||
Customer: 'Customer',
|
||||
CustomerRepresentative: 'CustomerRepresentative',
|
||||
RepresentativeAuthorization: 'RepresentativeAuthorization',
|
||||
Address: 'Address',
|
||||
BankCard: 'BankCard',
|
||||
IdentityDocument: 'IdentityDocument',
|
||||
|
|
@ -696,7 +861,11 @@ exports.Prisma.ModelName = {
|
|||
MobileContractDetails: 'MobileContractDetails',
|
||||
SimCard: 'SimCard',
|
||||
TvContractDetails: 'TvContractDetails',
|
||||
CarInsuranceDetails: 'CarInsuranceDetails'
|
||||
CarInsuranceDetails: 'CarInsuranceDetails',
|
||||
AuditLog: 'AuditLog',
|
||||
CustomerConsent: 'CustomerConsent',
|
||||
DataDeletionRequest: 'DataDeletionRequest',
|
||||
AuditRetentionPolicy: 'AuditRetentionPolicy'
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE `AuditLog` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`userId` INTEGER NULL,
|
||||
`userEmail` VARCHAR(191) NOT NULL,
|
||||
`userRole` VARCHAR(191) NULL,
|
||||
`customerId` INTEGER NULL,
|
||||
`isCustomerPortal` BOOLEAN NOT NULL DEFAULT false,
|
||||
`action` ENUM('CREATE', 'READ', 'UPDATE', 'DELETE', 'EXPORT', 'ANONYMIZE', 'LOGIN', 'LOGOUT', 'LOGIN_FAILED') NOT NULL,
|
||||
`sensitivity` ENUM('LOW', 'MEDIUM', 'HIGH', 'CRITICAL') NOT NULL DEFAULT 'MEDIUM',
|
||||
`resourceType` VARCHAR(191) NOT NULL,
|
||||
`resourceId` VARCHAR(191) NULL,
|
||||
`resourceLabel` VARCHAR(191) NULL,
|
||||
`endpoint` VARCHAR(191) NOT NULL,
|
||||
`httpMethod` VARCHAR(191) NOT NULL,
|
||||
`ipAddress` VARCHAR(191) NOT NULL,
|
||||
`userAgent` TEXT NULL,
|
||||
`changesBefore` LONGTEXT NULL,
|
||||
`changesAfter` LONGTEXT NULL,
|
||||
`changesEncrypted` BOOLEAN NOT NULL DEFAULT false,
|
||||
`dataSubjectId` INTEGER NULL,
|
||||
`legalBasis` VARCHAR(191) NULL,
|
||||
`success` BOOLEAN NOT NULL DEFAULT true,
|
||||
`errorMessage` TEXT NULL,
|
||||
`durationMs` INTEGER NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`hash` VARCHAR(191) NULL,
|
||||
`previousHash` VARCHAR(191) NULL,
|
||||
|
||||
INDEX `AuditLog_userId_idx`(`userId`),
|
||||
INDEX `AuditLog_customerId_idx`(`customerId`),
|
||||
INDEX `AuditLog_resourceType_resourceId_idx`(`resourceType`, `resourceId`),
|
||||
INDEX `AuditLog_dataSubjectId_idx`(`dataSubjectId`),
|
||||
INDEX `AuditLog_action_idx`(`action`),
|
||||
INDEX `AuditLog_createdAt_idx`(`createdAt`),
|
||||
INDEX `AuditLog_sensitivity_idx`(`sensitivity`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `CustomerConsent` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`customerId` INTEGER NOT NULL,
|
||||
`consentType` ENUM('DATA_PROCESSING', 'MARKETING_EMAIL', 'MARKETING_PHONE', 'DATA_SHARING_PARTNER') NOT NULL,
|
||||
`status` ENUM('GRANTED', 'WITHDRAWN', 'PENDING') NOT NULL DEFAULT 'PENDING',
|
||||
`grantedAt` DATETIME(3) NULL,
|
||||
`withdrawnAt` DATETIME(3) NULL,
|
||||
`source` VARCHAR(191) NULL,
|
||||
`documentPath` VARCHAR(191) NULL,
|
||||
`version` VARCHAR(191) NULL,
|
||||
`ipAddress` VARCHAR(191) NULL,
|
||||
`createdBy` VARCHAR(191) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `CustomerConsent_customerId_idx`(`customerId`),
|
||||
INDEX `CustomerConsent_consentType_idx`(`consentType`),
|
||||
INDEX `CustomerConsent_status_idx`(`status`),
|
||||
UNIQUE INDEX `CustomerConsent_customerId_consentType_key`(`customerId`, `consentType`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `DataDeletionRequest` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`customerId` INTEGER NOT NULL,
|
||||
`status` ENUM('PENDING', 'IN_PROGRESS', 'COMPLETED', 'PARTIALLY_COMPLETED', 'REJECTED') NOT NULL DEFAULT 'PENDING',
|
||||
`requestedAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`requestSource` VARCHAR(191) NOT NULL,
|
||||
`requestedBy` VARCHAR(191) NOT NULL,
|
||||
`processedAt` DATETIME(3) NULL,
|
||||
`processedBy` VARCHAR(191) NULL,
|
||||
`deletedData` LONGTEXT NULL,
|
||||
`retainedData` LONGTEXT NULL,
|
||||
`retentionReason` TEXT NULL,
|
||||
`proofDocument` VARCHAR(191) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `DataDeletionRequest_customerId_idx`(`customerId`),
|
||||
INDEX `DataDeletionRequest_status_idx`(`status`),
|
||||
INDEX `DataDeletionRequest_requestedAt_idx`(`requestedAt`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `AuditRetentionPolicy` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`resourceType` VARCHAR(191) NOT NULL,
|
||||
`sensitivity` ENUM('LOW', 'MEDIUM', 'HIGH', 'CRITICAL') NULL,
|
||||
`retentionDays` INTEGER NOT NULL,
|
||||
`description` VARCHAR(191) NULL,
|
||||
`legalBasis` VARCHAR(191) NULL,
|
||||
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `AuditRetentionPolicy_resourceType_sensitivity_key`(`resourceType`, `sensitivity`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CustomerConsent` ADD CONSTRAINT `CustomerConsent_customerId_fkey` FOREIGN KEY (`customerId`) REFERENCES `Customer`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE `Customer` ADD COLUMN `consentHash` VARCHAR(191) NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `User` ADD COLUMN `whatsappNumber` VARCHAR(191) NULL,
|
||||
ADD COLUMN `telegramUsername` VARCHAR(191) NULL,
|
||||
ADD COLUMN `signalNumber` VARCHAR(191) NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX `Customer_consentHash_key` ON `Customer`(`consentHash`);
|
||||
|
|
@ -7,6 +7,36 @@ datasource db {
|
|||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ==================== EMAIL LOG ====================
|
||||
|
||||
model EmailLog {
|
||||
id Int @id @default(autoincrement())
|
||||
// Absender & Empfänger
|
||||
fromAddress String // Absender-E-Mail
|
||||
toAddress String // Empfänger-E-Mail
|
||||
subject String // Betreff
|
||||
// Versand-Kontext
|
||||
context String // z.B. "consent-link", "authorization-request", "customer-email"
|
||||
customerId Int? // Zugehöriger Kunde (falls vorhanden)
|
||||
triggeredBy String? // Wer hat den Versand ausgelöst (User-Email)
|
||||
// SMTP-Details
|
||||
smtpServer String // SMTP-Server
|
||||
smtpPort Int // SMTP-Port
|
||||
smtpEncryption String // SSL, STARTTLS, NONE
|
||||
smtpUser String // SMTP-Benutzername
|
||||
// Ergebnis
|
||||
success Boolean // Erfolgreich?
|
||||
messageId String? // Message-ID aus SMTP-Antwort
|
||||
errorMessage String? @db.Text // Fehlermeldung bei Fehler
|
||||
smtpResponse String? @db.Text // SMTP-Server-Antwort
|
||||
// Zeitstempel
|
||||
sentAt DateTime @default(now())
|
||||
|
||||
@@index([sentAt])
|
||||
@@index([customerId])
|
||||
@@index([success])
|
||||
}
|
||||
|
||||
// ==================== APP SETTINGS ====================
|
||||
|
||||
model AppSetting {
|
||||
|
|
@ -27,6 +57,12 @@ model User {
|
|||
lastName String
|
||||
isActive Boolean @default(true)
|
||||
tokenInvalidatedAt DateTime? // Zeitpunkt ab dem alle Tokens ungültig sind (für Zwangslogout bei Rechteänderung)
|
||||
|
||||
// Messaging-Kanäle (für Datenschutz-Link-Versand)
|
||||
whatsappNumber String?
|
||||
telegramUsername String?
|
||||
signalNumber String?
|
||||
|
||||
customerId Int? @unique
|
||||
customer Customer? @relation(fields: [customerId], references: [id])
|
||||
roles UserRole[]
|
||||
|
|
@ -97,6 +133,7 @@ model Customer {
|
|||
commercialRegisterPath String? // PDF-Pfad zum Handelsregisterauszug
|
||||
commercialRegisterNumber String? // Handelsregisternummer (Text)
|
||||
privacyPolicyPath String? // PDF-Pfad zur Datenschutzerklärung (für alle Kunden)
|
||||
consentHash String? @unique // Permanenter Hash für öffentlichen Einwilligungslink /datenschutz/<hash>
|
||||
notes String? @db.Text
|
||||
|
||||
// ===== Portal-Zugangsdaten =====
|
||||
|
|
@ -118,6 +155,13 @@ model Customer {
|
|||
representingFor CustomerRepresentative[] @relation("RepresentativeCustomer")
|
||||
representedBy CustomerRepresentative[] @relation("RepresentedCustomer")
|
||||
|
||||
// Vollmachten
|
||||
authorizationsGiven RepresentativeAuthorization[] @relation("AuthorizationCustomer")
|
||||
authorizationsReceived RepresentativeAuthorization[] @relation("AuthorizationRepresentative")
|
||||
|
||||
// DSGVO: Einwilligungen
|
||||
consents CustomerConsent[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
|
@ -140,6 +184,28 @@ model CustomerRepresentative {
|
|||
@@unique([customerId, representativeId]) // Keine doppelten Einträge
|
||||
}
|
||||
|
||||
// ==================== VOLLMACHTEN ====================
|
||||
// Vollmacht: Kunde B erteilt Kunde A die Vollmacht, seine Daten einzusehen
|
||||
// Ohne Vollmacht kann der Vertreter die Verträge des Kunden NICHT sehen
|
||||
|
||||
model RepresentativeAuthorization {
|
||||
id Int @id @default(autoincrement())
|
||||
customerId Int // Der Kunde, der die Vollmacht erteilt (z.B. Mutter)
|
||||
customer Customer @relation("AuthorizationCustomer", fields: [customerId], references: [id], onDelete: Cascade)
|
||||
representativeId Int // Der Vertreter, der Zugriff bekommt (z.B. Sohn)
|
||||
representative Customer @relation("AuthorizationRepresentative", fields: [representativeId], references: [id], onDelete: Cascade)
|
||||
isGranted Boolean @default(false) // Vollmacht erteilt?
|
||||
grantedAt DateTime? // Wann erteilt
|
||||
withdrawnAt DateTime? // Wann widerrufen
|
||||
source String? // Quelle: 'portal', 'papier', 'crm-backend'
|
||||
documentPath String? // PDF-Upload der unterschriebenen Vollmacht
|
||||
notes String? @db.Text // Notizen
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([customerId, representativeId]) // Eine Vollmacht pro Paar
|
||||
}
|
||||
|
||||
// ==================== ADDRESSES ====================
|
||||
|
||||
enum AddressType {
|
||||
|
|
@ -247,6 +313,10 @@ model EmailProviderConfig {
|
|||
smtpEncryption MailEncryption @default(SSL) // SSL, STARTTLS oder NONE
|
||||
allowSelfSignedCerts Boolean @default(false) // Selbstsignierte Zertifikate erlauben
|
||||
|
||||
// System-E-Mail für automatisierte Nachrichten (z.B. DSGVO Consent-Links)
|
||||
systemEmailAddress String? // z.B. "info@stressfrei-wechseln.de"
|
||||
systemEmailPasswordEncrypted String? // Passwort (verschlüsselt)
|
||||
|
||||
isActive Boolean @default(true)
|
||||
isDefault Boolean @default(false) // Standard-Provider
|
||||
createdAt DateTime @default(now())
|
||||
|
|
@ -356,14 +426,25 @@ model Meter {
|
|||
}
|
||||
|
||||
model MeterReading {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
meterId Int
|
||||
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
|
||||
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
|
||||
readingDate DateTime
|
||||
value Float
|
||||
unit String @default("kWh")
|
||||
unit String @default("kWh")
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
// Meldung & Übertragung
|
||||
reportedBy String? // Wer hat gemeldet? (E-Mail des Portal-Kunden oder Mitarbeiter)
|
||||
status MeterReadingStatus @default(RECORDED)
|
||||
transferredAt DateTime? // Wann wurde der Stand an den Anbieter übertragen?
|
||||
transferredBy String? // Wer hat übertragen?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
enum MeterReadingStatus {
|
||||
RECORDED // Erfasst (vom Mitarbeiter)
|
||||
REPORTED // Vom Kunden gemeldet (Portal)
|
||||
TRANSFERRED // An Anbieter übertragen
|
||||
}
|
||||
|
||||
// ==================== SALES PLATFORMS ====================
|
||||
|
|
@ -759,3 +840,170 @@ model CarInsuranceDetails {
|
|||
policyNumber String?
|
||||
previousInsurer String?
|
||||
}
|
||||
|
||||
// ==================== AUDIT LOGGING (DSGVO) ====================
|
||||
|
||||
enum AuditAction {
|
||||
CREATE
|
||||
READ
|
||||
UPDATE
|
||||
DELETE
|
||||
EXPORT // DSGVO-Datenexport
|
||||
ANONYMIZE // Recht auf Vergessenwerden
|
||||
LOGIN
|
||||
LOGOUT
|
||||
LOGIN_FAILED
|
||||
}
|
||||
|
||||
enum AuditSensitivity {
|
||||
LOW // Einstellungen, Plattformen
|
||||
MEDIUM // Verträge, Tarife
|
||||
HIGH // Kundendaten, Bankdaten
|
||||
CRITICAL // Authentifizierung, Ausweisdokumente
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
// Wer
|
||||
userId Int? // Staff User (null bei Kundenportal/System)
|
||||
userEmail String
|
||||
userRole String? @db.Text // Rolle zum Zeitpunkt der Aktion
|
||||
customerId Int? // Bei Kundenportal-Zugriff
|
||||
isCustomerPortal Boolean @default(false)
|
||||
|
||||
// Was
|
||||
action AuditAction
|
||||
sensitivity AuditSensitivity @default(MEDIUM)
|
||||
|
||||
// Welche Ressource
|
||||
resourceType String // Prisma Model Name
|
||||
resourceId String? // ID des Datensatzes
|
||||
resourceLabel String? // Lesbare Bezeichnung
|
||||
|
||||
// Kontext
|
||||
endpoint String // API-Pfad
|
||||
httpMethod String // GET, POST, PUT, DELETE
|
||||
ipAddress String
|
||||
userAgent String? @db.Text
|
||||
|
||||
// Änderungen (JSON, bei sensiblen Daten verschlüsselt)
|
||||
changesBefore String? @db.LongText
|
||||
changesAfter String? @db.LongText
|
||||
changesEncrypted Boolean @default(false)
|
||||
|
||||
// DSGVO
|
||||
dataSubjectId Int? // Betroffene Person (für Reports)
|
||||
legalBasis String? // Rechtsgrundlage
|
||||
|
||||
// Status
|
||||
success Boolean @default(true)
|
||||
errorMessage String? @db.Text
|
||||
durationMs Int?
|
||||
|
||||
// Unveränderlichkeit (Hash-Kette)
|
||||
createdAt DateTime @default(now())
|
||||
hash String? // SHA-256 Hash des Eintrags
|
||||
previousHash String? // Hash des vorherigen Eintrags
|
||||
|
||||
@@index([userId])
|
||||
@@index([customerId])
|
||||
@@index([resourceType, resourceId])
|
||||
@@index([dataSubjectId])
|
||||
@@index([action])
|
||||
@@index([createdAt])
|
||||
@@index([sensitivity])
|
||||
}
|
||||
|
||||
// ==================== CONSENT MANAGEMENT (DSGVO) ====================
|
||||
|
||||
enum ConsentType {
|
||||
DATA_PROCESSING // Grundlegende Datenverarbeitung
|
||||
MARKETING_EMAIL // E-Mail-Marketing
|
||||
MARKETING_PHONE // Telefon-Marketing
|
||||
DATA_SHARING_PARTNER // Weitergabe an Partner
|
||||
}
|
||||
|
||||
enum ConsentStatus {
|
||||
GRANTED
|
||||
WITHDRAWN
|
||||
PENDING
|
||||
}
|
||||
|
||||
model CustomerConsent {
|
||||
id Int @id @default(autoincrement())
|
||||
customerId Int
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
|
||||
consentType ConsentType
|
||||
status ConsentStatus @default(PENDING)
|
||||
|
||||
grantedAt DateTime?
|
||||
withdrawnAt DateTime?
|
||||
source String? // "portal", "telefon", "papier", "email"
|
||||
documentPath String? // Unterschriebenes Dokument
|
||||
version String? // Version der Datenschutzerklärung
|
||||
ipAddress String?
|
||||
|
||||
createdBy String // User der die Einwilligung erfasst hat
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([customerId, consentType])
|
||||
@@index([customerId])
|
||||
@@index([consentType])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
// ==================== DATA DELETION REQUESTS (DSGVO) ====================
|
||||
|
||||
enum DeletionRequestStatus {
|
||||
PENDING // Anfrage eingegangen
|
||||
IN_PROGRESS // Wird bearbeitet
|
||||
COMPLETED // Abgeschlossen
|
||||
PARTIALLY_COMPLETED // Teildaten behalten (rechtliche Gründe)
|
||||
REJECTED // Abgelehnt
|
||||
}
|
||||
|
||||
model DataDeletionRequest {
|
||||
id Int @id @default(autoincrement())
|
||||
customerId Int
|
||||
|
||||
status DeletionRequestStatus @default(PENDING)
|
||||
requestedAt DateTime @default(now())
|
||||
requestSource String // "email", "portal", "brief"
|
||||
requestedBy String // Wer hat angefragt
|
||||
|
||||
processedAt DateTime?
|
||||
processedBy String? // Mitarbeiter der bearbeitet hat
|
||||
|
||||
deletedData String? @db.LongText // JSON: Was wurde gelöscht
|
||||
retainedData String? @db.LongText // JSON: Was wurde behalten + Grund
|
||||
retentionReason String? @db.Text // Begründung für Aufbewahrung
|
||||
|
||||
proofDocument String? // Pfad zum Löschnachweis-PDF
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([customerId])
|
||||
@@index([status])
|
||||
@@index([requestedAt])
|
||||
}
|
||||
|
||||
// ==================== AUDIT RETENTION POLICIES ====================
|
||||
|
||||
model AuditRetentionPolicy {
|
||||
id Int @id @default(autoincrement())
|
||||
resourceType String // "*" für Standard, oder spezifischer Model-Name
|
||||
sensitivity AuditSensitivity?
|
||||
retentionDays Int // Aufbewahrungsfrist in Tagen (z.B. 3650 = 10 Jahre)
|
||||
description String?
|
||||
legalBasis String? // Gesetzliche Grundlage (z.B. "AO §147", "HGB §257")
|
||||
isActive Boolean @default(true)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([resourceType, sensitivity])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
|
|
@ -26,6 +27,9 @@ async function main() {
|
|||
// Spezial-Permissions
|
||||
developer: ['access'],
|
||||
emails: ['delete'],
|
||||
// DSGVO & Audit
|
||||
audit: ['read', 'export', 'admin'],
|
||||
gdpr: ['export', 'delete', 'admin'],
|
||||
};
|
||||
|
||||
const permissions: { resource: string; action: string }[] = [];
|
||||
|
|
@ -60,10 +64,42 @@ async function main() {
|
|||
(p) => p.resource === 'providers' && p.action === 'read'
|
||||
);
|
||||
|
||||
// Helper: Sync permissions for a role (adds missing, removes excess)
|
||||
async function syncRolePermissions(roleId: number, permissionIds: number[]) {
|
||||
const existing = await prisma.rolePermission.findMany({
|
||||
where: { roleId },
|
||||
select: { permissionId: true },
|
||||
});
|
||||
const existingIds = new Set(existing.map((e) => e.permissionId));
|
||||
const targetIds = new Set(permissionIds);
|
||||
|
||||
// Add missing permissions
|
||||
const missing = permissionIds.filter((id) => !existingIds.has(id));
|
||||
if (missing.length > 0) {
|
||||
await prisma.rolePermission.createMany({
|
||||
data: missing.map((permissionId) => ({ roleId, permissionId })),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
console.log(` → ${missing.length} Permissions hinzugefügt für Rolle #${roleId}`);
|
||||
}
|
||||
|
||||
// Remove excess permissions
|
||||
const excess = existing.filter((e) => !targetIds.has(e.permissionId)).map((e) => e.permissionId);
|
||||
if (excess.length > 0) {
|
||||
await prisma.rolePermission.deleteMany({
|
||||
where: { roleId, permissionId: { in: excess } },
|
||||
});
|
||||
console.log(` → ${excess.length} Permissions entfernt für Rolle #${roleId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create roles
|
||||
// Admin - all permissions EXCEPT developer:access (that's controlled separately)
|
||||
// Admin - all permissions EXCEPT developer:access and audit/gdpr (controlled separately via checkboxes)
|
||||
const adminPermissions = allPermissions.filter(
|
||||
(p) => !(p.resource === 'developer' && p.action === 'access')
|
||||
(p) =>
|
||||
!(p.resource === 'developer' && p.action === 'access') &&
|
||||
p.resource !== 'audit' &&
|
||||
p.resource !== 'gdpr'
|
||||
);
|
||||
const adminRole = await prisma.role.upsert({
|
||||
where: { name: 'Admin' },
|
||||
|
|
@ -76,8 +112,10 @@ async function main() {
|
|||
},
|
||||
},
|
||||
});
|
||||
await syncRolePermissions(adminRole.id, adminPermissions.map((p) => p.id));
|
||||
|
||||
// Developer - ALL permissions including developer:access
|
||||
// Developer - ALL permissions (developer:access + alles andere)
|
||||
const developerPermissions = allPermissions;
|
||||
const developerRole = await prisma.role.upsert({
|
||||
where: { name: 'Developer' },
|
||||
update: {},
|
||||
|
|
@ -85,10 +123,28 @@ async function main() {
|
|||
name: 'Developer',
|
||||
description: 'Voller Zugriff inkl. Entwickler-Tools',
|
||||
permissions: {
|
||||
create: allPermissions.map((p) => ({ permissionId: p.id })),
|
||||
create: developerPermissions.map((p) => ({ permissionId: p.id })),
|
||||
},
|
||||
},
|
||||
});
|
||||
await syncRolePermissions(developerRole.id, developerPermissions.map((p) => p.id));
|
||||
|
||||
// DSGVO - audit and gdpr permissions (hidden role, controlled via hasGdprAccess)
|
||||
const gdprPermissions = allPermissions.filter(
|
||||
(p) => p.resource === 'audit' || p.resource === 'gdpr'
|
||||
);
|
||||
const gdprRole = await prisma.role.upsert({
|
||||
where: { name: 'DSGVO' },
|
||||
update: {},
|
||||
create: {
|
||||
name: 'DSGVO',
|
||||
description: 'DSGVO-Zugriff: Audit-Logs und Datenschutz-Verwaltung',
|
||||
permissions: {
|
||||
create: gdprPermissions.map((p) => ({ permissionId: p.id })),
|
||||
},
|
||||
},
|
||||
});
|
||||
await syncRolePermissions(gdprRole.id, gdprPermissions.map((p) => p.id));
|
||||
|
||||
// Employee - full access to customers, contracts, read access to lookup tables
|
||||
const employeePermIds = allPermissions
|
||||
|
|
@ -119,6 +175,7 @@ async function main() {
|
|||
},
|
||||
},
|
||||
});
|
||||
await syncRolePermissions(employeeRole.id, employeePermIds);
|
||||
|
||||
// Read-only employee - read access to main entities and lookup tables
|
||||
const readOnlyResources = [
|
||||
|
|
@ -146,6 +203,7 @@ async function main() {
|
|||
},
|
||||
},
|
||||
});
|
||||
await syncRolePermissions(readOnlyRole.id, readOnlyPermIds);
|
||||
|
||||
// Customer role - read own data only (handled in middleware)
|
||||
const customerRole = await prisma.role.upsert({
|
||||
|
|
@ -159,6 +217,7 @@ async function main() {
|
|||
},
|
||||
},
|
||||
});
|
||||
await syncRolePermissions(customerRole.id, readOnlyPermIds);
|
||||
|
||||
console.log('Roles created');
|
||||
|
||||
|
|
@ -346,6 +405,91 @@ async function main() {
|
|||
|
||||
console.log('App settings created');
|
||||
|
||||
// ==================== AUDIT RETENTION POLICIES (DSGVO) ====================
|
||||
// Standard-Policy (ohne Sensitivity)
|
||||
const existingDefault = await prisma.auditRetentionPolicy.findFirst({
|
||||
where: { resourceType: '*', sensitivity: null },
|
||||
});
|
||||
if (!existingDefault) {
|
||||
await prisma.auditRetentionPolicy.create({
|
||||
data: {
|
||||
resourceType: '*',
|
||||
sensitivity: null,
|
||||
retentionDays: 3650, // 10 Jahre
|
||||
description: 'Standard-Aufbewahrungsfrist',
|
||||
legalBasis: 'AO §147, HGB §257',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Spezifische Policies mit Sensitivity
|
||||
const specificPolicies = [
|
||||
{
|
||||
resourceType: 'Authentication',
|
||||
sensitivity: 'CRITICAL' as const,
|
||||
retentionDays: 730, // 2 Jahre
|
||||
description: 'Login-Versuche und Authentifizierung',
|
||||
legalBasis: 'Sicherheitsanforderungen',
|
||||
},
|
||||
{
|
||||
resourceType: 'Customer',
|
||||
sensitivity: 'HIGH' as const,
|
||||
retentionDays: 3650, // 10 Jahre
|
||||
description: 'Kundendaten-Zugriffe',
|
||||
legalBasis: 'Steuerrecht (AO §147)',
|
||||
},
|
||||
{
|
||||
resourceType: 'Contract',
|
||||
sensitivity: 'MEDIUM' as const,
|
||||
retentionDays: 3650, // 10 Jahre
|
||||
description: 'Vertragsdaten-Zugriffe',
|
||||
legalBasis: 'Steuerrecht (AO §147)',
|
||||
},
|
||||
{
|
||||
resourceType: 'AppSetting',
|
||||
sensitivity: 'LOW' as const,
|
||||
retentionDays: 1095, // 3 Jahre
|
||||
description: 'Allgemeine Einstellungen',
|
||||
legalBasis: 'Verjährungsfrist (BGB §195)',
|
||||
},
|
||||
];
|
||||
|
||||
for (const policy of specificPolicies) {
|
||||
await prisma.auditRetentionPolicy.upsert({
|
||||
where: {
|
||||
resourceType_sensitivity: {
|
||||
resourceType: policy.resourceType,
|
||||
sensitivity: policy.sensitivity,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
retentionDays: policy.retentionDays,
|
||||
description: policy.description,
|
||||
legalBasis: policy.legalBasis,
|
||||
},
|
||||
create: policy,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Audit retention policies created');
|
||||
|
||||
// ==================== CONSENT HASH FÜR BESTEHENDE KUNDEN ====================
|
||||
const customersWithoutHash = await prisma.customer.findMany({
|
||||
where: { consentHash: null },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
for (const c of customersWithoutHash) {
|
||||
await prisma.customer.update({
|
||||
where: { id: c.id },
|
||||
data: { consentHash: crypto.randomUUID() },
|
||||
});
|
||||
}
|
||||
|
||||
if (customersWithoutHash.length > 0) {
|
||||
console.log(`ConsentHash für ${customersWithoutHash.length} Kunden generiert`);
|
||||
}
|
||||
|
||||
console.log('Seeding completed!');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
/**
|
||||
* Script: Datenschutzerklärung in die Datenbank einfügen
|
||||
* Ausführen: npx tsx scripts/seed-privacy-policy.ts
|
||||
*/
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const privacyPolicyHtml = `
|
||||
<h1 style="text-align: center; margin-bottom: 0.25em;">Datenschutzerklärung</h1>
|
||||
<p style="text-align: center; color: #6b7280; font-size: 0.9em; margin-top: 0;">gemäß EU-Datenschutz-Grundverordnung (DSGVO)</p>
|
||||
|
||||
<div style="background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; padding: 16px 20px; margin: 24px 0;">
|
||||
<p style="margin: 0; font-weight: 600; color: #1e40af; font-size: 1.05em;">Hacker-Net Telekommunikation – Stefan Hacker</p>
|
||||
<p style="margin: 4px 0 0 0; color: #1e3a5f; font-size: 0.9em;">
|
||||
Am Wunderburgpark 5b, 26135 Oldenburg<br>
|
||||
Tel.: 01735837852 · E-Mail: info@hacker-net.de
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 24px;">Sehr geehrte(r) {{anrede}} {{vorname}} {{nachname}},</p>
|
||||
|
||||
<p style="line-height: 1.7;">der Schutz Ihrer persönlichen Daten ist uns ein wichtiges Anliegen. Nachfolgend informieren wir Sie darüber, wie wir Ihre Daten erheben, verarbeiten und schützen.</p>
|
||||
|
||||
<hr style="border: none; border-top: 2px solid #e5e7eb; margin: 36px 0;">
|
||||
|
||||
<h2 style="margin-bottom: 16px;">1. Verantwortlicher</h2>
|
||||
|
||||
<p style="line-height: 1.7;">Verantwortlich für die Datenverarbeitung im Sinne der DSGVO ist:</p>
|
||||
|
||||
<p style="padding: 12px 16px; border-left: 3px solid #3b82f6; line-height: 1.8; margin: 20px 0;">
|
||||
<strong>Hacker-Net Telekommunikation – Stefan Hacker</strong><br>
|
||||
Am Wunderburgpark 5b, 26135 Oldenburg<br>
|
||||
Tel.: 01735837852<br>
|
||||
E-Mail: info@hacker-net.de
|
||||
</p>
|
||||
|
||||
<hr style="border: none; border-top: 2px solid #e5e7eb; margin: 36px 0;">
|
||||
|
||||
<h2 style="margin-bottom: 16px;">2. Erhebung, Zweck und Speicherung Ihrer Daten</h2>
|
||||
|
||||
<h3 style="margin-top: 28px; margin-bottom: 12px;">Welche Daten wir erheben</h3>
|
||||
|
||||
<p style="line-height: 1.7;">Wenn Sie uns beauftragen, erheben wir folgende personenbezogene Daten:</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px 24px; margin: 20px 0;">
|
||||
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">●</span> Anrede, Vorname, Nachname</div>
|
||||
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">●</span> Geburtsdatum, Geburtsort</div>
|
||||
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">●</span> Ausweisdaten</div>
|
||||
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">●</span> Bankdaten (IBAN, BIC)</div>
|
||||
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">●</span> E-Mail-Adresse</div>
|
||||
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">●</span> Anschrift / Lieferanschriften</div>
|
||||
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">●</span> Telefonnummer(n)</div>
|
||||
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">●</span> Vertragsdaten / Produkte</div>
|
||||
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">●</span> Zählernummern (Strom, Gas)</div>
|
||||
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">●</span> Fahrzeugschein- / Führerscheindaten</div>
|
||||
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">●</span> Domainnamen / E-Mail-Adressen</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #fefce8; border: 1px solid #fde68a; border-radius: 8px; padding: 16px 20px; margin: 28px 0;">
|
||||
<p style="margin: 0 0 8px 0; font-weight: 600; color: #92400e;">Personalisierte E-Mail-Adresse</p>
|
||||
<p style="margin: 0; font-size: 0.9em; color: #78350f; line-height: 1.7;">
|
||||
Im Rahmen unserer Dienstleistung erstellen wir für Sie eine personalisierte E-Mail-Adresse mit der Endung <strong>@stressfrei-wechseln.de</strong>.
|
||||
</p>
|
||||
<p style="margin: 10px 0 0 0; font-size: 0.9em; color: #78350f; line-height: 1.7;">
|
||||
Diese dient als Verteiler-Adresse zur Kommunikation mit Anbietern – sowohl Sie als auch wir erhalten darüber die relevante Korrespondenz.
|
||||
</p>
|
||||
<p style="margin: 10px 0 0 0; font-size: 0.9em; color: #78350f; line-height: 1.7;">
|
||||
Die Adresse wird ausschließlich für diesen Zweck verwendet und nach Beendigung des Vertragsverhältnisses deaktiviert.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 32px; margin-bottom: 12px;">Zweck der Datenerhebung</h3>
|
||||
|
||||
<p style="line-height: 1.7;">Die Erhebung dieser Daten erfolgt zu folgenden Zwecken:</p>
|
||||
|
||||
<ul style="line-height: 2; margin: 16px 0;">
|
||||
<li>Identifikation als unser Kunde</li>
|
||||
<li>Angemessene Beratung und Betreuung</li>
|
||||
<li>Korrespondenz mit Ihnen und mit Anbietern in Ihrem Auftrag</li>
|
||||
<li>Rechnungsstellung</li>
|
||||
<li>Kündigung von Altverträgen und Abschluss neuer Verträge</li>
|
||||
</ul>
|
||||
|
||||
<h3 style="margin-top: 32px; margin-bottom: 12px;">Rechtsgrundlage</h3>
|
||||
|
||||
<p style="line-height: 1.7;">Die Datenverarbeitung erfolgt auf Grundlage von:</p>
|
||||
|
||||
<ul style="line-height: 2; margin: 16px 0;">
|
||||
<li><strong>Art. 6 Abs. 1 S. 1 lit. b DSGVO</strong> – Vertragserfüllung</li>
|
||||
<li><strong>Art. 6 Abs. 1 S. 1 lit. a DSGVO</strong> – Ihre Einwilligung, soweit erteilt</li>
|
||||
</ul>
|
||||
|
||||
<h3 style="margin-top: 32px; margin-bottom: 12px;">Speicherdauer</h3>
|
||||
|
||||
<p style="line-height: 1.7;">Ihre Daten werden für die Dauer des Vertragsverhältnisses gespeichert.</p>
|
||||
|
||||
<p style="line-height: 1.7;">Nach Vertragsende bewahren wir Ihre Daten gemäß den gesetzlichen Aufbewahrungsfristen auf (<strong>10 Jahre</strong> gemäß §§ 147 AO, 257 HGB) und löschen sie anschließend, sofern keine darüber hinausgehende Einwilligung vorliegt.</p>
|
||||
|
||||
<hr style="border: none; border-top: 2px solid #e5e7eb; margin: 36px 0;">
|
||||
|
||||
<h2 style="margin-bottom: 16px;">3. Weitergabe von Daten an Dritte</h2>
|
||||
|
||||
<p style="line-height: 1.7;">Ihre personenbezogenen Daten werden nur weitergegeben, wenn dies für die Erfüllung unseres Vertrags mit Ihnen erforderlich ist (<strong>Art. 6 Abs. 1 S. 1 lit. b DSGVO</strong>).</p>
|
||||
|
||||
<p style="line-height: 1.7;">Hierzu gehört insbesondere die Weitergabe an <strong>Produkt- und Dienstleistungsanbieter</strong> (z. B. Telekommunikations-, Strom-, Gas- und Versicherungsanbieter), da ohne diese Weitergabe keine Verträge gekündigt und/oder neue Verträge abgeschlossen werden können.</p>
|
||||
|
||||
<p style="line-height: 1.7;">Die weitergegebenen Daten dürfen von den Empfängern ausschließlich zu den genannten Zwecken verwendet werden.</p>
|
||||
|
||||
<p style="line-height: 1.7;">Eine darüber hinausgehende Weitergabe findet ohne Ihre ausdrückliche Einwilligung nicht statt.</p>
|
||||
|
||||
<hr style="border: none; border-top: 2px solid #e5e7eb; margin: 36px 0;">
|
||||
|
||||
<h2 style="margin-bottom: 16px;">4. Sicherheit Ihrer Daten</h2>
|
||||
|
||||
<p style="line-height: 1.7;">Wir setzen technische und organisatorische Maßnahmen ein, um Ihre Daten gegen Verlust, Zerstörung, Manipulation und unberechtigten Zugriff zu schützen:</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 24px 0;">
|
||||
<div style="background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 8px; padding: 14px 16px;">
|
||||
<p style="margin: 0; font-weight: 600; font-size: 0.9em; color: #166534;">Verschlüsselung</p>
|
||||
<p style="margin: 6px 0 0 0; font-size: 0.85em; color: #15803d; line-height: 1.5;">SSL/TLS-verschlüsselte Datenübertragung</p>
|
||||
</div>
|
||||
<div style="background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 8px; padding: 14px 16px;">
|
||||
<p style="margin: 0; font-weight: 600; font-size: 0.9em; color: #166534;">Zugangskontrolle</p>
|
||||
<p style="margin: 6px 0 0 0; font-size: 0.85em; color: #15803d; line-height: 1.5;">Berechtigungssysteme und Passwortschutz</p>
|
||||
</div>
|
||||
<div style="background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 8px; padding: 14px 16px;">
|
||||
<p style="margin: 0; font-weight: 600; font-size: 0.9em; color: #166534;">Updates</p>
|
||||
<p style="margin: 6px 0 0 0; font-size: 0.85em; color: #15803d; line-height: 1.5;">Regelmäßige Sicherheitsupdates unserer Systeme</p>
|
||||
</div>
|
||||
<div style="background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 8px; padding: 14px 16px;">
|
||||
<p style="margin: 0; font-weight: 600; font-size: 0.9em; color: #166534;">Vertraulichkeit</p>
|
||||
<p style="margin: 6px 0 0 0; font-size: 0.85em; color: #15803d; line-height: 1.5;">Verpflichtung aller Mitarbeiter zur Verschwiegenheit</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr style="border: none; border-top: 2px solid #e5e7eb; margin: 36px 0;">
|
||||
|
||||
<h2 style="margin-bottom: 16px;">5. Ihre Rechte</h2>
|
||||
|
||||
<p style="line-height: 1.7;">Als betroffene Person stehen Ihnen gemäß DSGVO folgende Rechte zu:</p>
|
||||
|
||||
<div style="margin: 24px 0;">
|
||||
<div style="border-left: 3px solid #8b5cf6; padding: 12px 16px; margin-bottom: 12px; background: #faf5ff;">
|
||||
<p style="margin: 0;"><strong>Widerruf der Einwilligung</strong> <span style="color: #6b7280; font-size: 0.85em;">(Art. 7 Abs. 3 DSGVO)</span></p>
|
||||
<p style="margin: 8px 0 0 0; font-size: 0.9em; color: #4b5563; line-height: 1.6;">Sie können eine erteilte Einwilligung jederzeit widerrufen. Die Rechtmäßigkeit der bis zum Widerruf erfolgten Datenverarbeitung bleibt davon unberührt.</p>
|
||||
</div>
|
||||
|
||||
<div style="border-left: 3px solid #8b5cf6; padding: 12px 16px; margin-bottom: 12px; background: #faf5ff;">
|
||||
<p style="margin: 0;"><strong>Auskunftsrecht</strong> <span style="color: #6b7280; font-size: 0.85em;">(Art. 15 DSGVO)</span></p>
|
||||
<p style="margin: 8px 0 0 0; font-size: 0.9em; color: #4b5563; line-height: 1.6;">Sie können Auskunft über Ihre von uns verarbeiteten Daten verlangen – einschließlich Verarbeitungszwecke, Kategorien, Empfänger, Speicherdauer und Herkunft.</p>
|
||||
</div>
|
||||
|
||||
<div style="border-left: 3px solid #8b5cf6; padding: 12px 16px; margin-bottom: 12px; background: #faf5ff;">
|
||||
<p style="margin: 0;"><strong>Recht auf Berichtigung</strong> <span style="color: #6b7280; font-size: 0.85em;">(Art. 16 DSGVO)</span></p>
|
||||
<p style="margin: 8px 0 0 0; font-size: 0.9em; color: #4b5563; line-height: 1.6;">Sie können die Berichtigung unrichtiger oder die Vervollständigung unvollständiger Daten verlangen.</p>
|
||||
</div>
|
||||
|
||||
<div style="border-left: 3px solid #8b5cf6; padding: 12px 16px; margin-bottom: 12px; background: #faf5ff;">
|
||||
<p style="margin: 0;"><strong>Recht auf Löschung</strong> <span style="color: #6b7280; font-size: 0.85em;">(Art. 17 DSGVO)</span></p>
|
||||
<p style="margin: 8px 0 0 0; font-size: 0.9em; color: #4b5563; line-height: 1.6;">Sie können die Löschung Ihrer Daten verlangen, sofern keine gesetzlichen Aufbewahrungspflichten entgegenstehen.</p>
|
||||
</div>
|
||||
|
||||
<div style="border-left: 3px solid #8b5cf6; padding: 12px 16px; margin-bottom: 12px; background: #faf5ff;">
|
||||
<p style="margin: 0;"><strong>Recht auf Einschränkung</strong> <span style="color: #6b7280; font-size: 0.85em;">(Art. 18 DSGVO)</span></p>
|
||||
<p style="margin: 8px 0 0 0; font-size: 0.9em; color: #4b5563; line-height: 1.6;">Sie können unter bestimmten Voraussetzungen die Einschränkung der Verarbeitung verlangen.</p>
|
||||
</div>
|
||||
|
||||
<div style="border-left: 3px solid #8b5cf6; padding: 12px 16px; margin-bottom: 12px; background: #faf5ff;">
|
||||
<p style="margin: 0;"><strong>Recht auf Datenübertragbarkeit</strong> <span style="color: #6b7280; font-size: 0.85em;">(Art. 20 DSGVO)</span></p>
|
||||
<p style="margin: 8px 0 0 0; font-size: 0.9em; color: #4b5563; line-height: 1.6;">Sie können Ihre Daten in einem strukturierten, gängigen und maschinenlesbaren Format erhalten oder an einen anderen Verantwortlichen übermitteln lassen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; padding: 16px 20px; margin: 28px 0;">
|
||||
<p style="margin: 0 0 8px 0; font-weight: 600; color: #991b1b;">Beschwerderecht (Art. 77 DSGVO)</p>
|
||||
<p style="margin: 0; font-size: 0.9em; color: #7f1d1d; line-height: 1.6;">
|
||||
Sie haben das Recht, sich bei der zuständigen Aufsichtsbehörde zu beschweren:
|
||||
</p>
|
||||
<p style="margin: 12px 0 0 0; font-size: 0.9em; color: #7f1d1d; line-height: 1.8;">
|
||||
<strong>Die Landesbeauftragte für den Datenschutz Niedersachsen</strong><br>
|
||||
Prinzenstraße 5, 30159 Hannover<br>
|
||||
Tel.: 0511 120-4500<br>
|
||||
E-Mail: poststelle@lfd.niedersachsen.de
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr style="border: none; border-top: 2px solid #e5e7eb; margin: 36px 0;">
|
||||
|
||||
<h2 style="margin-bottom: 16px;">6. Widerspruchsrecht</h2>
|
||||
|
||||
<p style="line-height: 1.7;">Sofern Ihre Daten auf Grundlage von berechtigten Interessen gemäß <strong>Art. 6 Abs. 1 S. 1 lit. f DSGVO</strong> verarbeitet werden, haben Sie das Recht, gemäß <strong>Art. 21 DSGVO</strong> Widerspruch gegen die Verarbeitung einzulegen, soweit Gründe vorliegen, die sich aus Ihrer besonderen Situation ergeben.</p>
|
||||
|
||||
<div style="background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; padding: 20px; margin: 28px 0; text-align: center;">
|
||||
<p style="margin: 0 0 8px 0; font-size: 0.95em; color: #1e40af; line-height: 1.6;">
|
||||
Möchten Sie eines Ihrer Rechte ausüben?
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 0.95em; color: #1e40af;">
|
||||
Schreiben Sie uns einfach eine E-Mail an:
|
||||
</p>
|
||||
<p style="margin: 12px 0 0 0;">
|
||||
<strong style="font-size: 1.15em; color: #1e40af;">info@hacker-net.de</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 36px 0 16px 0;">
|
||||
|
||||
<p style="text-align: center; color: #9ca3af; font-size: 0.85em;">Stand: {{datum}} · Kundennummer: {{kundennummer}}</p>
|
||||
`;
|
||||
|
||||
async function main() {
|
||||
await prisma.appSetting.upsert({
|
||||
where: { key: 'privacyPolicyHtml' },
|
||||
update: { value: privacyPolicyHtml },
|
||||
create: { key: 'privacyPolicyHtml', value: privacyPolicyHtml },
|
||||
});
|
||||
|
||||
console.log('Datenschutzerklärung erfolgreich gespeichert!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
import { Response } from 'express';
|
||||
import { AuthRequest } from '../types/index.js';
|
||||
import * as auditService from '../services/audit.service.js';
|
||||
import { AuditAction, AuditSensitivity } from '@prisma/client';
|
||||
|
||||
/**
|
||||
* Audit-Logs mit Filtern abrufen
|
||||
*/
|
||||
export async function getAuditLogs(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const {
|
||||
userId,
|
||||
customerId,
|
||||
dataSubjectId,
|
||||
action,
|
||||
sensitivity,
|
||||
resourceType,
|
||||
resourceId,
|
||||
startDate,
|
||||
endDate,
|
||||
success,
|
||||
search,
|
||||
page,
|
||||
limit,
|
||||
} = req.query;
|
||||
|
||||
const result = await auditService.searchAuditLogs({
|
||||
userId: userId ? parseInt(userId as string) : undefined,
|
||||
customerId: customerId ? parseInt(customerId as string) : undefined,
|
||||
dataSubjectId: dataSubjectId ? parseInt(dataSubjectId as string) : undefined,
|
||||
action: action as AuditAction | undefined,
|
||||
sensitivity: sensitivity as AuditSensitivity | undefined,
|
||||
resourceType: resourceType as string | undefined,
|
||||
resourceId: resourceId as string | undefined,
|
||||
startDate: startDate ? new Date(startDate as string) : undefined,
|
||||
endDate: endDate ? new Date(endDate as string) : undefined,
|
||||
success: success !== undefined ? success === 'true' : undefined,
|
||||
search: search as string | undefined,
|
||||
page: page ? parseInt(page as string) : 1,
|
||||
limit: limit ? parseInt(limit as string) : 50,
|
||||
});
|
||||
|
||||
res.json({ success: true, ...result });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Audit-Logs:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Audit-Logs' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelnes Audit-Log abrufen
|
||||
*/
|
||||
export async function getAuditLogById(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const log = await auditService.getAuditLogById(id);
|
||||
|
||||
if (!log) {
|
||||
return res.status(404).json({ success: false, error: 'Audit-Log nicht gefunden' });
|
||||
}
|
||||
|
||||
res.json({ success: true, data: log });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen des Audit-Logs:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Abrufen des Audit-Logs' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit-Logs für einen Kunden abrufen (DSGVO)
|
||||
*/
|
||||
export async function getAuditLogsByCustomer(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
const logs = await auditService.getAuditLogsByDataSubject(customerId);
|
||||
|
||||
res.json({ success: true, data: logs });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Kunden-Audit-Logs:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Audit-Logs' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit-Logs exportieren
|
||||
*/
|
||||
export async function exportAuditLogs(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const format = (req.query.format as 'json' | 'csv') || 'json';
|
||||
const {
|
||||
action,
|
||||
sensitivity,
|
||||
resourceType,
|
||||
startDate,
|
||||
endDate,
|
||||
} = req.query;
|
||||
|
||||
const content = await auditService.exportAuditLogs(
|
||||
{
|
||||
action: action as AuditAction | undefined,
|
||||
sensitivity: sensitivity as AuditSensitivity | undefined,
|
||||
resourceType: resourceType as string | undefined,
|
||||
startDate: startDate ? new Date(startDate as string) : undefined,
|
||||
endDate: endDate ? new Date(endDate as string) : undefined,
|
||||
},
|
||||
format
|
||||
);
|
||||
|
||||
const contentType = format === 'csv' ? 'text/csv' : 'application/json';
|
||||
const filename = `audit-logs-${new Date().toISOString().split('T')[0]}.${format}`;
|
||||
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
res.send(content);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Exportieren der Audit-Logs:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Exportieren' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash-Ketten-Integrität prüfen
|
||||
*/
|
||||
export async function verifyIntegrity(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const { fromId, toId } = req.query;
|
||||
|
||||
const result = await auditService.verifyIntegrity(
|
||||
fromId ? parseInt(fromId as string) : undefined,
|
||||
toId ? parseInt(toId as string) : undefined
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
valid: result.valid,
|
||||
checkedCount: result.checkedCount,
|
||||
invalidEntries: result.invalidEntries,
|
||||
message: result.valid
|
||||
? 'Alle Einträge sind valide'
|
||||
: `${result.invalidEntries.length} manipulierte Einträge gefunden`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Integritätsprüfung:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler bei der Integritätsprüfung' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retention-Policies abrufen
|
||||
*/
|
||||
export async function getRetentionPolicies(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const policies = await auditService.getRetentionPolicies();
|
||||
res.json({ success: true, data: policies });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Retention-Policies:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Policies' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retention-Policy aktualisieren
|
||||
*/
|
||||
export async function updateRetentionPolicy(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const { retentionDays, description, legalBasis, isActive } = req.body;
|
||||
|
||||
const policy = await auditService.updateRetentionPolicy(id, {
|
||||
retentionDays,
|
||||
description,
|
||||
legalBasis,
|
||||
isActive,
|
||||
});
|
||||
|
||||
res.json({ success: true, data: policy });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Retention-Policy:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Aktualisieren' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retention-Cleanup manuell ausführen
|
||||
*/
|
||||
export async function runRetentionCleanup(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const result = await auditService.runRetentionCleanup();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: `${result.deletedCount} alte Audit-Logs wurden gelöscht`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Retention-Cleanup:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Cleanup' });
|
||||
}
|
||||
}
|
||||
|
|
@ -317,7 +317,12 @@ export async function sendEmailFromAccount(req: Request, res: Response): Promise
|
|||
};
|
||||
|
||||
// E-Mail senden
|
||||
const result = await sendEmail(credentials, stressfreiEmail.email, emailParams);
|
||||
const authReq = req as any;
|
||||
const result = await sendEmail(credentials, stressfreiEmail.email, emailParams, {
|
||||
context: 'customer-email',
|
||||
customerId: stressfreiEmail.customerId,
|
||||
triggeredBy: authReq.user?.email,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
res.status(400).json({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
import { Request, Response } from 'express';
|
||||
import * as consentPublicService from '../services/consent-public.service.js';
|
||||
import { createAuditLog } from '../services/audit.service.js';
|
||||
import { CONSENT_TYPE_LABELS } from '../services/consent.service.js';
|
||||
import { ConsentType } from '@prisma/client';
|
||||
|
||||
/**
|
||||
* Öffentliche Consent-Seite: Kundendaten + Datenschutztext + Status
|
||||
*/
|
||||
export async function getConsentPage(req: Request, res: Response) {
|
||||
try {
|
||||
const { hash } = req.params;
|
||||
|
||||
const result = await consentPublicService.getCustomerByConsentHash(hash);
|
||||
if (!result) {
|
||||
return res.status(404).json({ success: false, error: 'Ungültiger Link' });
|
||||
}
|
||||
|
||||
const privacyPolicyHtml = await consentPublicService.getPrivacyPolicyHtml(result.customer.id);
|
||||
|
||||
// Consent-Status mit Labels
|
||||
const consentsWithLabels = result.consents.map((c) => ({
|
||||
consentType: c.consentType,
|
||||
status: c.status,
|
||||
label: CONSENT_TYPE_LABELS[c.consentType as ConsentType]?.label || c.consentType,
|
||||
description: CONSENT_TYPE_LABELS[c.consentType as ConsentType]?.description || '',
|
||||
grantedAt: c.grantedAt,
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
customer: {
|
||||
firstName: result.customer.firstName,
|
||||
lastName: result.customer.lastName,
|
||||
customerNumber: result.customer.customerNumber,
|
||||
},
|
||||
privacyPolicyHtml,
|
||||
consents: consentsWithLabels,
|
||||
allGranted: consentsWithLabels.every((c) => c.status === 'GRANTED'),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Consent-Seite:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle 4 Einwilligungen erteilen (öffentlicher Link)
|
||||
*/
|
||||
export async function grantAllConsents(req: Request, res: Response) {
|
||||
try {
|
||||
const { hash } = req.params;
|
||||
const ipAddress = req.ip || req.socket.remoteAddress || 'unknown';
|
||||
|
||||
const results = await consentPublicService.grantAllConsentsPublic(hash, ipAddress);
|
||||
|
||||
// Audit-Log (manuell, da keine Auth-Middleware)
|
||||
const customer = await consentPublicService.getCustomerByConsentHash(hash);
|
||||
if (customer) {
|
||||
for (const type of Object.values(ConsentType)) {
|
||||
await createAuditLog({
|
||||
userEmail: customer.customer.email || 'public-link',
|
||||
action: 'UPDATE',
|
||||
sensitivity: 'HIGH',
|
||||
resourceType: 'CustomerConsent',
|
||||
resourceId: `${customer.customer.id}:${type}`,
|
||||
resourceLabel: `Einwilligung ${type} erteilt via Public-Link`,
|
||||
endpoint: `/api/public/consent/${hash}/grant`,
|
||||
httpMethod: 'POST',
|
||||
ipAddress,
|
||||
dataSubjectId: customer.customer.id,
|
||||
legalBasis: 'DSGVO Art. 6 Abs. 1 lit. a',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, data: results });
|
||||
} catch (error: any) {
|
||||
console.error('Fehler beim Erteilen der Einwilligungen:', error);
|
||||
res.status(400).json({ success: false, error: error.message || 'Fehler beim Erteilen' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Datenschutzerklärung als PDF
|
||||
*/
|
||||
export async function getConsentPdf(req: Request, res: Response) {
|
||||
try {
|
||||
const { hash } = req.params;
|
||||
|
||||
const result = await consentPublicService.getCustomerByConsentHash(hash);
|
||||
if (!result) {
|
||||
return res.status(404).json({ success: false, error: 'Ungültiger Link' });
|
||||
}
|
||||
|
||||
const pdfBuffer = await consentPublicService.generateConsentPdf(result.customer.id);
|
||||
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', 'inline; filename="datenschutzerklaerung.pdf"');
|
||||
res.send(pdfBuffer);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Generieren des PDFs:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Generieren' });
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { PrismaClient } from '@prisma/client';
|
|||
import * as contractService from '../services/contract.service.js';
|
||||
import * as contractCockpitService from '../services/contractCockpit.service.js';
|
||||
import * as contractHistoryService from '../services/contractHistory.service.js';
|
||||
import * as authorizationService from '../services/authorization.service.js';
|
||||
import { ApiResponse, AuthRequest } from '../types/index.js';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
|
@ -20,11 +21,19 @@ export async function getContracts(req: AuthRequest, res: Response): Promise<voi
|
|||
return;
|
||||
}
|
||||
|
||||
// Für Kundenportal-Benutzer: nur eigene + vertretene Kunden-Verträge anzeigen
|
||||
// Für Kundenportal-Benutzer: nur eigene + vertretene Kunden MIT Vollmacht
|
||||
let customerIds: number[] | undefined;
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
// Eigene Customer-ID + alle vertretenen Kunden-IDs
|
||||
customerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
// Eigene Customer-ID immer
|
||||
customerIds = [req.user.customerId];
|
||||
// Vertretene Kunden nur wenn Vollmacht erteilt
|
||||
const representedIds: number[] = req.user.representedCustomerIds || [];
|
||||
for (const repCustId of representedIds) {
|
||||
const hasAuth = await authorizationService.hasAuthorization(repCustId, req.user.customerId);
|
||||
if (hasAuth) {
|
||||
customerIds.push(repCustId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await contractService.getAllContracts({
|
||||
|
|
@ -60,9 +69,16 @@ export async function getContract(req: AuthRequest, res: Response): Promise<void
|
|||
return;
|
||||
}
|
||||
|
||||
// Für Kundenportal-Benutzer: Zugriff nur auf eigene + vertretene Kunden-Verträge
|
||||
// Für Kundenportal-Benutzer: Zugriff nur auf eigene + vertretene Kunden MIT Vollmacht
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
const allowedCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
const allowedCustomerIds = [req.user.customerId];
|
||||
const representedIds: number[] = req.user.representedCustomerIds || [];
|
||||
for (const repCustId of representedIds) {
|
||||
const hasAuth = await authorizationService.hasAuthorization(repCustId, req.user.customerId);
|
||||
if (hasAuth) {
|
||||
allowedCustomerIds.push(repCustId);
|
||||
}
|
||||
}
|
||||
if (!allowedCustomerIds.includes(contract.customerId)) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as customerService from '../services/customer.service.js';
|
||||
import * as authService from '../services/auth.service.js';
|
||||
import { ApiResponse } from '../types/index.js';
|
||||
import { ApiResponse, AuthRequest } from '../types/index.js';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Customer CRUD
|
||||
export async function getCustomers(req: Request, res: Response): Promise<void> {
|
||||
|
|
@ -331,6 +334,98 @@ export async function deleteMeterReading(req: Request, res: Response): Promise<v
|
|||
}
|
||||
}
|
||||
|
||||
// ==================== PORTAL: ZÄHLERSTAND MELDEN ====================
|
||||
|
||||
export async function reportMeterReading(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const user = req.user as any;
|
||||
if (!user?.isCustomerPortal || !user?.customerId) {
|
||||
res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const meterId = parseInt(req.params.meterId);
|
||||
const { value, readingDate, notes } = req.body;
|
||||
|
||||
// Prüfe ob der Zähler zum Kunden gehört
|
||||
const meter = await prisma.meter.findUnique({
|
||||
where: { id: meterId },
|
||||
select: { customerId: true },
|
||||
});
|
||||
|
||||
if (!meter || meter.customerId !== user.customerId) {
|
||||
res.status(403).json({ success: false, error: 'Kein Zugriff auf diesen Zähler' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const reading = await prisma.meterReading.create({
|
||||
data: {
|
||||
meterId,
|
||||
value: parseFloat(value),
|
||||
readingDate: readingDate ? new Date(readingDate) : new Date(),
|
||||
notes,
|
||||
reportedBy: user.email,
|
||||
status: 'REPORTED',
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, data: reading } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Melden des Zählerstands',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMyMeters(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const user = req.user as any;
|
||||
if (!user?.isCustomerPortal || !user?.customerId) {
|
||||
res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const meters = await prisma.meter.findMany({
|
||||
where: { customerId: user.customerId, isActive: true },
|
||||
include: {
|
||||
readings: {
|
||||
orderBy: { readingDate: 'desc' },
|
||||
take: 5,
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
|
||||
res.json({ success: true, data: meters } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden der Zähler' } as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function markReadingTransferred(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const meterId = parseInt(req.params.meterId);
|
||||
const readingId = parseInt(req.params.readingId);
|
||||
|
||||
const reading = await prisma.meterReading.update({
|
||||
where: { id: readingId },
|
||||
data: {
|
||||
status: 'TRANSFERRED',
|
||||
transferredAt: new Date(),
|
||||
transferredBy: req.user?.email,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ success: true, data: reading } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PORTAL SETTINGS ====================
|
||||
|
||||
export async function getPortalSettings(req: Request, res: Response): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import { Response } from 'express';
|
||||
import { AuthRequest } from '../types/index.js';
|
||||
import * as emailLogService from '../services/emailLog.service.js';
|
||||
|
||||
export async function getEmailLogs(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const limit = parseInt(req.query.limit as string) || 50;
|
||||
const success = req.query.success !== undefined ? req.query.success === 'true' : undefined;
|
||||
const search = req.query.search as string || undefined;
|
||||
const context = req.query.context as string || undefined;
|
||||
|
||||
const result = await emailLogService.getEmailLogs({ page, limit, success, search, context });
|
||||
res.json({ success: true, ...result });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Email-Logs:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEmailLogStats(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const stats = await emailLogService.getEmailLogStats();
|
||||
res.json({ success: true, data: stats });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Email-Log-Stats:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEmailLogDetail(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const log = await emailLogService.getEmailLogById(id);
|
||||
if (!log) {
|
||||
return res.status(404).json({ success: false, error: 'Log-Eintrag nicht gefunden' });
|
||||
}
|
||||
res.json({ success: true, data: log });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden des Email-Log-Details:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,899 @@
|
|||
import { Response } from 'express';
|
||||
import { AuthRequest } from '../types/index.js';
|
||||
import * as gdprService from '../services/gdpr.service.js';
|
||||
import * as consentService from '../services/consent.service.js';
|
||||
import * as consentPublicService from '../services/consent-public.service.js';
|
||||
import * as appSettingService from '../services/appSetting.service.js';
|
||||
import { createAuditLog } from '../services/audit.service.js';
|
||||
import { ConsentType, DeletionRequestStatus, PrismaClient } from '@prisma/client';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { sendEmail, SmtpCredentials } from '../services/smtpService.js';
|
||||
import { getSystemEmailCredentials } from '../services/emailProvider/emailProviderService.js';
|
||||
import * as authorizationService from '../services/authorization.service.js';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/**
|
||||
* Kundendaten exportieren (DSGVO Art. 15)
|
||||
*/
|
||||
export async function exportCustomerData(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
const format = (req.query.format as string) || 'json';
|
||||
|
||||
const data = await gdprService.exportCustomerData(customerId);
|
||||
|
||||
// Audit-Log für Export
|
||||
await createAuditLog({
|
||||
userId: req.user?.userId,
|
||||
userEmail: req.user?.email || 'unknown',
|
||||
action: 'EXPORT',
|
||||
resourceType: 'GDPR',
|
||||
resourceId: customerId.toString(),
|
||||
resourceLabel: `Datenexport für ${data.dataSubject.name}`,
|
||||
endpoint: req.path,
|
||||
httpMethod: req.method,
|
||||
ipAddress: req.socket.remoteAddress || 'unknown',
|
||||
dataSubjectId: customerId,
|
||||
legalBasis: 'DSGVO Art. 15',
|
||||
});
|
||||
|
||||
if (format === 'json') {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
`attachment; filename="datenexport_${data.dataSubject.customerNumber}_${new Date().toISOString().split('T')[0]}.json"`
|
||||
);
|
||||
res.json(data);
|
||||
} else {
|
||||
// Für PDF würde hier PDFKit verwendet werden
|
||||
res.json({ success: true, data });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Datenexport:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Datenexport',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Löschanfrage erstellen
|
||||
*/
|
||||
export async function createDeletionRequest(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const customerId = parseInt(req.params.id);
|
||||
const { requestSource } = req.body;
|
||||
|
||||
const request = await gdprService.createDeletionRequest({
|
||||
customerId,
|
||||
requestSource: requestSource || 'portal',
|
||||
requestedBy: req.user?.email || 'unknown',
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, data: request });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Löschanfrage:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Löschanfragen abrufen
|
||||
*/
|
||||
export async function getDeletionRequests(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const { status, page, limit } = req.query;
|
||||
|
||||
const result = await gdprService.getDeletionRequests({
|
||||
status: status as DeletionRequestStatus | undefined,
|
||||
page: page ? parseInt(page as string) : 1,
|
||||
limit: limit ? parseInt(limit as string) : 20,
|
||||
});
|
||||
|
||||
res.json({ success: true, ...result });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Löschanfragen:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelne Löschanfrage abrufen
|
||||
*/
|
||||
export async function getDeletionRequest(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const request = await gdprService.getDeletionRequest(id);
|
||||
|
||||
if (!request) {
|
||||
return res.status(404).json({ success: false, error: 'Löschanfrage nicht gefunden' });
|
||||
}
|
||||
|
||||
res.json({ success: true, data: request });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Löschanfrage:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Löschanfrage bearbeiten
|
||||
*/
|
||||
export async function processDeletionRequest(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const { action, retentionReason } = req.body;
|
||||
|
||||
if (!['complete', 'partial', 'reject'].includes(action)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Ungültige Aktion. Erlaubt: complete, partial, reject',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await gdprService.processDeletionRequest(id, {
|
||||
processedBy: req.user?.email || 'unknown',
|
||||
action,
|
||||
retentionReason,
|
||||
});
|
||||
|
||||
// Audit-Log für Löschung
|
||||
await createAuditLog({
|
||||
userId: req.user?.userId,
|
||||
userEmail: req.user?.email || 'unknown',
|
||||
action: 'ANONYMIZE',
|
||||
resourceType: 'GDPR',
|
||||
resourceId: id.toString(),
|
||||
resourceLabel: `Löschanfrage ${action}`,
|
||||
endpoint: req.path,
|
||||
httpMethod: req.method,
|
||||
ipAddress: req.socket.remoteAddress || 'unknown',
|
||||
dataSubjectId: result.customerId,
|
||||
legalBasis: 'DSGVO Art. 17',
|
||||
});
|
||||
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Bearbeiten der Löschanfrage:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Bearbeiten',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Löschnachweis-PDF herunterladen
|
||||
*/
|
||||
export async function getDeletionProof(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const request = await gdprService.getDeletionRequest(id);
|
||||
|
||||
if (!request) {
|
||||
return res.status(404).json({ success: false, error: 'Löschanfrage nicht gefunden' });
|
||||
}
|
||||
|
||||
if (!request.proofDocument) {
|
||||
return res.status(404).json({ success: false, error: 'Kein Löschnachweis vorhanden' });
|
||||
}
|
||||
|
||||
const filepath = path.join(process.cwd(), 'uploads', request.proofDocument);
|
||||
|
||||
if (!fs.existsSync(filepath)) {
|
||||
return res.status(404).json({ success: false, error: 'Datei nicht gefunden' });
|
||||
}
|
||||
|
||||
res.download(filepath);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Download des Löschnachweises:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Download' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DSGVO-Dashboard Statistiken
|
||||
*/
|
||||
export async function getDashboardStats(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const stats = await gdprService.getGDPRDashboardStats();
|
||||
res.json({ success: true, data: stats });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Dashboard-Statistiken:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CONSENT ENDPOINTS ====================
|
||||
|
||||
/**
|
||||
* Einwilligungen eines Kunden abrufen
|
||||
*/
|
||||
export async function getCustomerConsents(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
const consents = await consentService.getCustomerConsents(customerId);
|
||||
|
||||
// Labels hinzufügen
|
||||
const consentsWithLabels = consents.map((c) => ({
|
||||
...c,
|
||||
label: consentService.CONSENT_TYPE_LABELS[c.consentType as ConsentType]?.label,
|
||||
description: consentService.CONSENT_TYPE_LABELS[c.consentType as ConsentType]?.description,
|
||||
}));
|
||||
|
||||
res.json({ success: true, data: consentsWithLabels });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Einwilligungen:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consent-Status prüfen (hat der Kunde vollständig zugestimmt?)
|
||||
*/
|
||||
export async function checkConsentStatus(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
const result = await consentService.hasFullConsent(customerId);
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Consent-Check:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Consent-Check' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Einwilligung aktualisieren (nur Kundenportal-Benutzer!)
|
||||
*/
|
||||
export async function updateCustomerConsent(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
const consentType = req.params.consentType as ConsentType;
|
||||
const { status, source, documentPath, version } = req.body;
|
||||
|
||||
// Nur Kundenportal-Benutzer dürfen Einwilligungen ändern
|
||||
if (!(req.user as any)?.isCustomerPortal) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Nur Kunden können Einwilligungen ändern',
|
||||
});
|
||||
}
|
||||
|
||||
// Portal: nur eigene + vertretene Kunden
|
||||
const allowed = [
|
||||
(req.user as any).customerId,
|
||||
...((req.user as any).representedCustomerIds || []),
|
||||
];
|
||||
if (!allowed.includes(customerId)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Keine Berechtigung für diesen Kunden',
|
||||
});
|
||||
}
|
||||
|
||||
if (!Object.values(ConsentType).includes(consentType)) {
|
||||
return res.status(400).json({ success: false, error: 'Ungültiger Consent-Typ' });
|
||||
}
|
||||
|
||||
const consent = await consentService.updateConsent(customerId, consentType, {
|
||||
status,
|
||||
source: source || 'portal',
|
||||
documentPath,
|
||||
version,
|
||||
ipAddress: req.socket.remoteAddress,
|
||||
createdBy: req.user?.email || 'unknown',
|
||||
});
|
||||
|
||||
res.json({ success: true, data: consent });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Einwilligung:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consent-Übersicht für Dashboard
|
||||
*/
|
||||
export async function getConsentOverview(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const overview = await consentService.getConsentOverview();
|
||||
|
||||
// Labels hinzufügen
|
||||
const overviewWithLabels = Object.entries(overview).map(([type, stats]) => ({
|
||||
type,
|
||||
label: consentService.CONSENT_TYPE_LABELS[type as ConsentType]?.label,
|
||||
description: consentService.CONSENT_TYPE_LABELS[type as ConsentType]?.description,
|
||||
...stats,
|
||||
}));
|
||||
|
||||
res.json({ success: true, data: overviewWithLabels });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Consent-Übersicht:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PRIVACY POLICY ENDPOINTS ====================
|
||||
|
||||
/**
|
||||
* Datenschutzerklärung abrufen (HTML)
|
||||
*/
|
||||
export async function getPrivacyPolicy(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const html = await appSettingService.getSetting('privacyPolicyHtml');
|
||||
res.json({ success: true, data: { html: html || '' } });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Datenschutzerklärung:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Datenschutzerklärung speichern (HTML)
|
||||
*/
|
||||
export async function updatePrivacyPolicy(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const { html } = req.body;
|
||||
|
||||
if (typeof html !== 'string') {
|
||||
return res.status(400).json({ success: false, error: 'HTML-Inhalt erforderlich' });
|
||||
}
|
||||
|
||||
await appSettingService.setSetting('privacyPolicyHtml', html);
|
||||
|
||||
res.json({ success: true, message: 'Datenschutzerklärung gespeichert' });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern der Datenschutzerklärung:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Speichern' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmacht-Vorlage abrufen (HTML)
|
||||
*/
|
||||
export async function getAuthorizationTemplate(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const html = await appSettingService.getSetting('authorizationTemplateHtml');
|
||||
res.json({ success: true, data: { html: html || '' } });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Vollmacht-Vorlage:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmacht-Vorlage speichern (HTML)
|
||||
*/
|
||||
export async function updateAuthorizationTemplate(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const { html } = req.body;
|
||||
|
||||
if (typeof html !== 'string') {
|
||||
return res.status(400).json({ success: false, error: 'HTML-Inhalt erforderlich' });
|
||||
}
|
||||
|
||||
await appSettingService.setSetting('authorizationTemplateHtml', html);
|
||||
|
||||
res.json({ success: true, message: 'Vollmacht-Vorlage gespeichert' });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern der Vollmacht-Vorlage:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Speichern' });
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SEND CONSENT LINK ====================
|
||||
|
||||
/**
|
||||
* Consent-Link an Kunden senden
|
||||
*/
|
||||
// ==================== PORTAL ENDPOINTS ====================
|
||||
|
||||
/**
|
||||
* Portal: Eigene Datenschutzseite (Privacy Policy + Consent-Status)
|
||||
*/
|
||||
export async function getMyPrivacy(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const user = req.user as any;
|
||||
if (!user?.isCustomerPortal || !user?.customerId) {
|
||||
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
|
||||
}
|
||||
|
||||
const customerId = user.customerId;
|
||||
|
||||
const [privacyPolicyHtml, consents] = await Promise.all([
|
||||
consentPublicService.getPrivacyPolicyHtml(customerId),
|
||||
consentService.getCustomerConsents(customerId),
|
||||
]);
|
||||
|
||||
const consentsWithLabels = consents.map((c) => ({
|
||||
...c,
|
||||
label: consentService.CONSENT_TYPE_LABELS[c.consentType as ConsentType]?.label,
|
||||
description: consentService.CONSENT_TYPE_LABELS[c.consentType as ConsentType]?.description,
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
privacyPolicyHtml,
|
||||
consents: consentsWithLabels,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Portal-Datenschutzseite:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Portal: Datenschutzerklärung als PDF
|
||||
*/
|
||||
export async function getMyPrivacyPdf(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const user = req.user as any;
|
||||
if (!user?.isCustomerPortal || !user?.customerId) {
|
||||
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
|
||||
}
|
||||
|
||||
const pdfBuffer = await consentPublicService.generateConsentPdf(user.customerId);
|
||||
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', 'inline; filename="datenschutzerklaerung.pdf"');
|
||||
res.send(pdfBuffer);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Generieren des Portal-PDFs:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Generieren' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Portal: Eigenen Consent-Status prüfen
|
||||
*/
|
||||
export async function getMyConsentStatus(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const user = req.user as any;
|
||||
if (!user?.isCustomerPortal || !user?.customerId) {
|
||||
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
|
||||
}
|
||||
const result = await consentService.hasFullConsent(user.customerId);
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Consent-Status:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Consent-Status' });
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendConsentLink(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
const { channel } = req.body; // 'email', 'whatsapp', 'telegram', 'signal'
|
||||
|
||||
// ConsentHash sicherstellen
|
||||
const hash = await consentPublicService.ensureConsentHash(customerId);
|
||||
const baseUrl = process.env.PUBLIC_URL || req.headers.origin || 'http://localhost:5173';
|
||||
const consentUrl = `${baseUrl}/datenschutz/${hash}`;
|
||||
|
||||
// Bei E-Mail: tatsächlich senden
|
||||
if (channel === 'email') {
|
||||
// Kunde laden
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
select: { id: true, firstName: true, lastName: true, email: true },
|
||||
});
|
||||
|
||||
if (!customer?.email) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Kunde hat keine E-Mail-Adresse hinterlegt',
|
||||
});
|
||||
}
|
||||
|
||||
// System-E-Mail-Credentials vom aktiven Provider holen
|
||||
const systemEmail = await getSystemEmailCredentials();
|
||||
if (!systemEmail) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Keine System-E-Mail konfiguriert. Bitte in den Email-Provider-Einstellungen eine System-E-Mail-Adresse und Passwort hinterlegen.',
|
||||
});
|
||||
}
|
||||
|
||||
const credentials: SmtpCredentials = {
|
||||
host: systemEmail.smtpServer,
|
||||
port: systemEmail.smtpPort,
|
||||
user: systemEmail.emailAddress,
|
||||
password: systemEmail.password,
|
||||
encryption: systemEmail.smtpEncryption,
|
||||
allowSelfSignedCerts: systemEmail.allowSelfSignedCerts,
|
||||
};
|
||||
|
||||
// E-Mail zusammenstellen
|
||||
const emailHtml = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #1e40af;">Datenschutzerklärung – Ihre Zustimmung</h2>
|
||||
<p>Sehr geehrte(r) ${customer.firstName} ${customer.lastName},</p>
|
||||
<p>
|
||||
um Sie optimal beraten und betreuen zu können, benötigen wir Ihre Zustimmung zu unserer Datenschutzerklärung.
|
||||
</p>
|
||||
<p>
|
||||
Bitte klicken Sie auf den folgenden Button, um unsere Datenschutzerklärung einzusehen und Ihre Einwilligung zu erteilen:
|
||||
</p>
|
||||
<p style="text-align: center; margin: 32px 0;">
|
||||
<a href="${consentUrl}"
|
||||
style="background-color: #2563eb; color: #ffffff; padding: 14px 32px; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 16px; display: inline-block;">
|
||||
Datenschutzerklärung ansehen & zustimmen
|
||||
</a>
|
||||
</p>
|
||||
<p style="color: #6b7280; font-size: 14px;">
|
||||
Alternativ können Sie auch diesen Link in Ihren Browser kopieren:<br>
|
||||
<a href="${consentUrl}" style="color: #2563eb; word-break: break-all;">${consentUrl}</a>
|
||||
</p>
|
||||
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 24px 0;">
|
||||
<p style="color: #9ca3af; font-size: 12px;">
|
||||
Hacker-Net Telekommunikation – Stefan Hacker<br>
|
||||
Am Wunderburgpark 5b, 26135 Oldenburg<br>
|
||||
info@hacker-net.de
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const result = await sendEmail(credentials, systemEmail.emailAddress, {
|
||||
to: customer.email,
|
||||
subject: 'Datenschutzerklärung – Bitte um Ihre Zustimmung',
|
||||
html: emailHtml,
|
||||
}, {
|
||||
context: 'consent-link',
|
||||
customerId,
|
||||
triggeredBy: req.user?.email,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `E-Mail-Versand fehlgeschlagen: ${result.error}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Audit-Log
|
||||
await createAuditLog({
|
||||
userId: req.user?.userId,
|
||||
userEmail: req.user?.email || 'unknown',
|
||||
action: 'READ',
|
||||
resourceType: 'CustomerConsent',
|
||||
resourceId: customerId.toString(),
|
||||
resourceLabel: `Consent-Link gesendet (${channel})`,
|
||||
endpoint: req.path,
|
||||
httpMethod: req.method,
|
||||
ipAddress: req.socket.remoteAddress || 'unknown',
|
||||
dataSubjectId: customerId,
|
||||
legalBasis: 'DSGVO Art. 6 Abs. 1a',
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
url: consentUrl,
|
||||
channel,
|
||||
hash,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Senden des Consent-Links:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Senden',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== VOLLMACHTEN ====================
|
||||
|
||||
/**
|
||||
* Vollmacht-Anfrage an Kunden senden (per E-Mail, WhatsApp, Telegram, Signal)
|
||||
*/
|
||||
export async function sendAuthorizationRequest(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
const representativeId = parseInt(req.params.representativeId);
|
||||
const { channel } = req.body;
|
||||
|
||||
// Kunde (Vollmachtgeber) laden
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
select: { id: true, firstName: true, lastName: true, email: true },
|
||||
});
|
||||
|
||||
// Vertreter (Bevollmächtigter) laden
|
||||
const representative = await prisma.customer.findUnique({
|
||||
where: { id: representativeId },
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
});
|
||||
|
||||
if (!customer || !representative) {
|
||||
return res.status(404).json({ success: false, error: 'Kunde oder Vertreter nicht gefunden' });
|
||||
}
|
||||
|
||||
const baseUrl = process.env.PUBLIC_URL || req.headers.origin || 'http://localhost:5173';
|
||||
const portalUrl = `${baseUrl}/privacy`;
|
||||
|
||||
// E-Mail senden
|
||||
if (channel === 'email') {
|
||||
if (!customer.email) {
|
||||
return res.status(400).json({ success: false, error: 'Kunde hat keine E-Mail-Adresse hinterlegt' });
|
||||
}
|
||||
|
||||
const systemEmail = await getSystemEmailCredentials();
|
||||
if (!systemEmail) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Keine System-E-Mail konfiguriert. Bitte in den Email-Provider-Einstellungen eine System-E-Mail-Adresse und Passwort hinterlegen.',
|
||||
});
|
||||
}
|
||||
|
||||
const credentials: SmtpCredentials = {
|
||||
host: systemEmail.smtpServer,
|
||||
port: systemEmail.smtpPort,
|
||||
user: systemEmail.emailAddress,
|
||||
password: systemEmail.password,
|
||||
encryption: systemEmail.smtpEncryption,
|
||||
allowSelfSignedCerts: systemEmail.allowSelfSignedCerts,
|
||||
};
|
||||
|
||||
const emailHtml = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #1e40af;">Vollmacht – Ihre Zustimmung erforderlich</h2>
|
||||
<p>Sehr geehrte(r) ${customer.firstName} ${customer.lastName},</p>
|
||||
<p>
|
||||
<strong>${representative.firstName} ${representative.lastName}</strong> möchte als Ihr Vertreter Zugriff auf Ihre Vertragsdaten erhalten.
|
||||
</p>
|
||||
<p>
|
||||
Damit dies möglich ist, benötigen wir Ihre Vollmacht. Sie können diese bequem über unser Kundenportal erteilen:
|
||||
</p>
|
||||
<p style="text-align: center; margin: 32px 0;">
|
||||
<a href="${portalUrl}"
|
||||
style="background-color: #2563eb; color: #ffffff; padding: 14px 32px; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 16px; display: inline-block;">
|
||||
Vollmacht im Portal erteilen
|
||||
</a>
|
||||
</p>
|
||||
<p style="color: #6b7280; font-size: 14px;">
|
||||
Alternativ können Sie auch diesen Link in Ihren Browser kopieren:<br>
|
||||
<a href="${portalUrl}" style="color: #2563eb; word-break: break-all;">${portalUrl}</a>
|
||||
</p>
|
||||
<p style="color: #6b7280; font-size: 14px;">
|
||||
Sie können die Vollmacht jederzeit im Portal unter "Datenschutz" widerrufen.
|
||||
</p>
|
||||
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 24px 0;">
|
||||
<p style="color: #9ca3af; font-size: 12px;">
|
||||
Hacker-Net Telekommunikation – Stefan Hacker<br>
|
||||
Am Wunderburgpark 5b, 26135 Oldenburg<br>
|
||||
info@hacker-net.de
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const result = await sendEmail(credentials, systemEmail.emailAddress, {
|
||||
to: customer.email,
|
||||
subject: `Vollmacht für ${representative.firstName} ${representative.lastName} – Bitte um Ihre Zustimmung`,
|
||||
html: emailHtml,
|
||||
}, {
|
||||
context: 'authorization-request',
|
||||
customerId,
|
||||
triggeredBy: req.user?.email,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `E-Mail-Versand fehlgeschlagen: ${result.error}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Messaging-Text für WhatsApp/Telegram/Signal
|
||||
const messageText = `Hallo ${customer.firstName}, ${representative.firstName} ${representative.lastName} möchte als Ihr Vertreter Zugriff auf Ihre Vertragsdaten. Bitte erteilen Sie die Vollmacht im Portal: ${portalUrl}`;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { channel, portalUrl, messageText },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Senden der Vollmacht-Anfrage:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Senden',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmachten für einen Kunden abrufen (Admin-Ansicht)
|
||||
*/
|
||||
export async function getAuthorizations(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
// Sicherstellen dass Einträge für alle aktiven Vertreter existieren
|
||||
await authorizationService.ensureAuthorizationEntries(customerId);
|
||||
const authorizations = await authorizationService.getAuthorizationsForCustomer(customerId);
|
||||
res.json({ success: true, data: authorizations });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Vollmachten:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden der Vollmachten' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmacht erteilen (Admin: z.B. Papier-Upload)
|
||||
*/
|
||||
export async function grantAuthorization(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
const representativeId = parseInt(req.params.representativeId);
|
||||
const { source, notes } = req.body;
|
||||
|
||||
const auth = await authorizationService.grantAuthorization(customerId, representativeId, {
|
||||
source: source || 'crm-backend',
|
||||
notes,
|
||||
});
|
||||
|
||||
res.json({ success: true, data: auth });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erteilen der Vollmacht:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erteilen der Vollmacht',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmacht widerrufen
|
||||
*/
|
||||
export async function withdrawAuthorization(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
const representativeId = parseInt(req.params.representativeId);
|
||||
|
||||
const auth = await authorizationService.withdrawAuthorization(customerId, representativeId);
|
||||
res.json({ success: true, data: auth });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Widerrufen der Vollmacht:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Widerrufen',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmacht-Dokument hochladen (PDF)
|
||||
*/
|
||||
export async function uploadAuthorizationDocument(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
const representativeId = parseInt(req.params.representativeId);
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' });
|
||||
}
|
||||
|
||||
const documentPath = `/uploads/authorizations/${req.file.filename}`;
|
||||
const auth = await authorizationService.updateAuthorizationDocument(
|
||||
customerId,
|
||||
representativeId,
|
||||
documentPath
|
||||
);
|
||||
|
||||
res.json({ success: true, data: auth });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Upload des Vollmacht-Dokuments:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Upload',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmacht-Dokument löschen
|
||||
*/
|
||||
export async function deleteAuthorizationDocument(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
const representativeId = parseInt(req.params.representativeId);
|
||||
|
||||
const auth = await authorizationService.deleteAuthorizationDocument(customerId, representativeId);
|
||||
res.json({ success: true, data: auth });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Vollmacht-Dokuments:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Portal: Eigene Vollmachten abrufen (welche Vertreter dürfen meine Daten sehen?)
|
||||
*/
|
||||
export async function getMyAuthorizations(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const user = req.user as any;
|
||||
if (!user?.isCustomerPortal || !user?.customerId) {
|
||||
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
|
||||
}
|
||||
|
||||
await authorizationService.ensureAuthorizationEntries(user.customerId);
|
||||
const authorizations = await authorizationService.getAuthorizationsForCustomer(user.customerId);
|
||||
res.json({ success: true, data: authorizations });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der eigenen Vollmachten:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Portal: Vollmacht erteilen/widerrufen
|
||||
*/
|
||||
export async function toggleMyAuthorization(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const user = req.user as any;
|
||||
if (!user?.isCustomerPortal || !user?.customerId) {
|
||||
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
|
||||
}
|
||||
|
||||
const representativeId = parseInt(req.params.representativeId);
|
||||
const { grant } = req.body;
|
||||
|
||||
let auth;
|
||||
if (grant) {
|
||||
auth = await authorizationService.grantAuthorization(user.customerId, representativeId, {
|
||||
source: 'portal',
|
||||
});
|
||||
} else {
|
||||
auth = await authorizationService.withdrawAuthorization(user.customerId, representativeId);
|
||||
}
|
||||
|
||||
res.json({ success: true, data: auth });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Ändern der Vollmacht:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Ändern',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Portal: Prüfe Vollmacht-Status für alle vertretenen Kunden
|
||||
*/
|
||||
export async function getMyAuthorizationStatus(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const user = req.user as any;
|
||||
if (!user?.isCustomerPortal || !user?.customerId) {
|
||||
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
|
||||
}
|
||||
|
||||
// IDs der Kunden die dieser Vertreter vertritt
|
||||
const representedIds: number[] = user.representedCustomerIds || [];
|
||||
|
||||
// Für jeden vertretenen Kunden prüfen ob Vollmacht erteilt
|
||||
const statuses: { customerId: number; hasAuthorization: boolean }[] = [];
|
||||
for (const custId of representedIds) {
|
||||
const has = await authorizationService.hasAuthorization(custId, user.customerId);
|
||||
statuses.push({ customerId: custId, hasAuthorization: has });
|
||||
}
|
||||
|
||||
res.json({ success: true, data: statuses });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Vollmacht-Status:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,12 @@ import emailProviderRoutes from './routes/emailProvider.routes.js';
|
|||
import cachedEmailRoutes from './routes/cachedEmail.routes.js';
|
||||
import invoiceRoutes from './routes/invoice.routes.js';
|
||||
import contractHistoryRoutes from './routes/contractHistory.routes.js';
|
||||
import auditLogRoutes from './routes/auditLog.routes.js';
|
||||
import gdprRoutes from './routes/gdpr.routes.js';
|
||||
import consentPublicRoutes from './routes/consent-public.routes.js';
|
||||
import emailLogRoutes from './routes/emailLog.routes.js';
|
||||
import { auditContextMiddleware } from './middleware/auditContext.js';
|
||||
import { auditMiddleware } from './middleware/audit.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
|
|
@ -36,9 +42,16 @@ const PORT = process.env.PORT || 3001;
|
|||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Audit-Logging Middleware (DSGVO-konform)
|
||||
app.use(auditContextMiddleware);
|
||||
app.use(auditMiddleware);
|
||||
|
||||
// Statische Dateien für Uploads
|
||||
app.use('/api/uploads', express.static(path.join(process.cwd(), 'uploads')));
|
||||
|
||||
// Öffentliche Routes (OHNE Authentifizierung)
|
||||
app.use('/api/public/consent', consentPublicRoutes);
|
||||
|
||||
// Routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/customers', customerRoutes);
|
||||
|
|
@ -63,6 +76,9 @@ app.use('/api/email-providers', emailProviderRoutes);
|
|||
app.use('/api', cachedEmailRoutes);
|
||||
app.use('/api/energy-details', invoiceRoutes);
|
||||
app.use('/api', contractHistoryRoutes);
|
||||
app.use('/api/audit-logs', auditLogRoutes);
|
||||
app.use('/api/gdpr', gdprRoutes);
|
||||
app.use('/api/email-logs', emailLogRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
import { PrismaClient, Prisma } from '@prisma/client';
|
||||
import { setBeforeValues, setAfterValues } from '../middleware/auditContext.js';
|
||||
|
||||
// Modelle die für Before/After-Tracking relevant sind
|
||||
const AUDITED_MODELS = [
|
||||
'Customer',
|
||||
'Contract',
|
||||
'Address',
|
||||
'BankCard',
|
||||
'IdentityDocument',
|
||||
'User',
|
||||
'Meter',
|
||||
'MeterReading',
|
||||
'StressfreiEmail',
|
||||
'Provider',
|
||||
'Tariff',
|
||||
'ContractCategory',
|
||||
'AppSetting',
|
||||
'CustomerConsent',
|
||||
];
|
||||
|
||||
// Sensible Felder die aus dem Audit-Log gefiltert werden
|
||||
const SENSITIVE_FIELDS = [
|
||||
'password',
|
||||
'passwordHash',
|
||||
'portalPasswordHash',
|
||||
'portalPasswordEncrypted',
|
||||
'emailPasswordEncrypted',
|
||||
'internetPasswordEncrypted',
|
||||
'sipPasswordEncrypted',
|
||||
'pin',
|
||||
'puk',
|
||||
'apiKey',
|
||||
];
|
||||
|
||||
/**
|
||||
* Filtert sensible Felder aus einem Objekt
|
||||
*/
|
||||
function filterSensitiveFields(obj: Record<string, unknown>): Record<string, unknown> {
|
||||
const filtered: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (SENSITIVE_FIELDS.includes(key)) {
|
||||
filtered[key] = '[REDACTED]';
|
||||
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
filtered[key] = filterSensitiveFields(value as Record<string, unknown>);
|
||||
} else {
|
||||
filtered[key] = value;
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein Model für Audit-Tracking relevant ist
|
||||
*/
|
||||
function isAuditedModel(model: string | undefined): boolean {
|
||||
return model !== undefined && AUDITED_MODELS.includes(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Prisma Client mit Audit-Middleware
|
||||
*/
|
||||
function createPrismaClient(): PrismaClient {
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Middleware für Before/After-Tracking
|
||||
prisma.$use(async (params: Prisma.MiddlewareParams, next: (params: Prisma.MiddlewareParams) => Promise<unknown>) => {
|
||||
const { model, action, args } = params;
|
||||
|
||||
// Nur relevante Modelle und Aktionen tracken
|
||||
if (!isAuditedModel(model)) {
|
||||
return next(params);
|
||||
}
|
||||
|
||||
// Update-Operationen: Vorherigen Stand abrufen
|
||||
if (action === 'update' || action === 'updateMany') {
|
||||
try {
|
||||
const modelDelegate = (prisma as unknown as Record<string, { findUnique: (args: unknown) => Promise<unknown> }>)[
|
||||
model!.charAt(0).toLowerCase() + model!.slice(1)
|
||||
];
|
||||
|
||||
if (modelDelegate && args?.where) {
|
||||
const before = await modelDelegate.findUnique({ where: args.where });
|
||||
if (before) {
|
||||
setBeforeValues(filterSensitiveFields(before as Record<string, unknown>));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fehler beim Abrufen des vorherigen Stands ignorieren
|
||||
}
|
||||
}
|
||||
|
||||
// Delete-Operationen: Datensatz vor dem Löschen abrufen
|
||||
if (action === 'delete' || action === 'deleteMany') {
|
||||
try {
|
||||
const modelDelegate = (prisma as unknown as Record<string, { findUnique: (args: unknown) => Promise<unknown> }>)[
|
||||
model!.charAt(0).toLowerCase() + model!.slice(1)
|
||||
];
|
||||
|
||||
if (modelDelegate && args?.where) {
|
||||
const before = await modelDelegate.findUnique({ where: args.where });
|
||||
if (before) {
|
||||
setBeforeValues(filterSensitiveFields(before as Record<string, unknown>));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fehler beim Abrufen ignorieren
|
||||
}
|
||||
}
|
||||
|
||||
// Operation ausführen
|
||||
const result = await next(params);
|
||||
|
||||
// Nach Update/Create: Neuen Stand speichern
|
||||
if ((action === 'update' || action === 'create') && result) {
|
||||
setAfterValues(filterSensitiveFields(result as Record<string, unknown>));
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
return prisma;
|
||||
}
|
||||
|
||||
// Singleton-Instanz
|
||||
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
|
||||
export default prisma;
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
import { Response, NextFunction } from 'express';
|
||||
import { AuditAction } from '@prisma/client';
|
||||
import { AuthRequest } from '../types/index.js';
|
||||
import { createAuditLog } from '../services/audit.service.js';
|
||||
import { getAuditContext, AuditContext } from './auditContext.js';
|
||||
|
||||
// Resource-Typ-Mapping basierend auf Route-Patterns
|
||||
const RESOURCE_MAPPING: Record<string, { type: string; extractId?: (req: AuthRequest) => string | undefined }> = {
|
||||
'/api/customers': { type: 'Customer', extractId: (req) => req.params.id || req.params.customerId },
|
||||
'/api/customers/*/bank-cards': { type: 'BankCard', extractId: (req) => req.params.bankCardId },
|
||||
'/api/customers/*/documents': { type: 'IdentityDocument', extractId: (req) => req.params.documentId },
|
||||
'/api/customers/*/addresses': { type: 'Address', extractId: (req) => req.params.addressId },
|
||||
'/api/customers/*/meters': { type: 'Meter', extractId: (req) => req.params.meterId },
|
||||
'/api/customers/*/consents': { type: 'CustomerConsent', extractId: (req) => req.params.type },
|
||||
'/api/contracts': { type: 'Contract', extractId: (req) => req.params.id },
|
||||
'/api/contracts/*/history': { type: 'ContractHistoryEntry', extractId: (req) => req.params.entryId },
|
||||
'/api/contracts/*/tasks': { type: 'ContractTask', extractId: (req) => req.params.taskId },
|
||||
'/api/users': { type: 'User', extractId: (req) => req.params.id },
|
||||
'/api/providers': { type: 'Provider', extractId: (req) => req.params.id },
|
||||
'/api/tariffs': { type: 'Tariff', extractId: (req) => req.params.id },
|
||||
'/api/platforms': { type: 'SalesPlatform', extractId: (req) => req.params.id },
|
||||
'/api/contract-categories': { type: 'ContractCategory', extractId: (req) => req.params.id },
|
||||
'/api/cancellation-periods': { type: 'CancellationPeriod', extractId: (req) => req.params.id },
|
||||
'/api/contract-durations': { type: 'ContractDuration', extractId: (req) => req.params.id },
|
||||
'/api/settings': { type: 'AppSetting', extractId: (req) => req.params.key },
|
||||
'/api/email-providers': { type: 'EmailProviderConfig', extractId: (req) => req.params.id },
|
||||
'/api/auth': { type: 'Authentication' },
|
||||
'/api/audit-logs': { type: 'AuditLog', extractId: (req) => req.params.id },
|
||||
'/api/gdpr': { type: 'GDPR' },
|
||||
};
|
||||
|
||||
// Routen die nicht geloggt werden sollen
|
||||
const EXCLUDED_ROUTES = [
|
||||
'/api/health',
|
||||
'/api/uploads',
|
||||
];
|
||||
|
||||
/**
|
||||
* Bestimmt die Aktion basierend auf HTTP-Methode und Erfolg
|
||||
*/
|
||||
function determineAction(method: string, path: string, success: boolean): AuditAction {
|
||||
// Spezielle Auth-Aktionen
|
||||
if (path.includes('/auth/login')) {
|
||||
return success ? 'LOGIN' : 'LOGIN_FAILED';
|
||||
}
|
||||
if (path.includes('/auth/logout')) {
|
||||
return 'LOGOUT';
|
||||
}
|
||||
|
||||
// Standard CRUD-Aktionen
|
||||
switch (method.toUpperCase()) {
|
||||
case 'GET':
|
||||
return 'READ';
|
||||
case 'POST':
|
||||
return 'CREATE';
|
||||
case 'PUT':
|
||||
case 'PATCH':
|
||||
return 'UPDATE';
|
||||
case 'DELETE':
|
||||
return 'DELETE';
|
||||
default:
|
||||
return 'READ';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet den passenden Resource-Typ für einen Pfad
|
||||
*/
|
||||
function findResourceMapping(path: string): { type: string; extractId?: (req: AuthRequest) => string | undefined } | null {
|
||||
// Exakte Matches zuerst prüfen
|
||||
for (const [pattern, mapping] of Object.entries(RESOURCE_MAPPING)) {
|
||||
// Konvertiere Pattern zu Regex
|
||||
const regexPattern = pattern
|
||||
.replace(/\*/g, '[^/]+')
|
||||
.replace(/\//g, '\\/');
|
||||
const regex = new RegExp(`^${regexPattern}(?:/|$)`);
|
||||
|
||||
if (regex.test(path)) {
|
||||
return mapping;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert die betroffene Kunden-ID für DSGVO-Tracking
|
||||
*/
|
||||
function extractDataSubjectId(req: AuthRequest): number | undefined {
|
||||
// Aus Route-Parameter
|
||||
const customerId = req.params.customerId || req.params.id;
|
||||
if (customerId && req.path.includes('/customers')) {
|
||||
return parseInt(customerId);
|
||||
}
|
||||
|
||||
// Aus Request-Body (bei Create)
|
||||
if (req.body?.customerId) {
|
||||
return parseInt(req.body.customerId);
|
||||
}
|
||||
|
||||
// Bei Kundenportal-Zugriff
|
||||
if (req.user?.customerId) {
|
||||
return req.user.customerId;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert die IP-Adresse des Clients
|
||||
*/
|
||||
function getClientIp(req: AuthRequest): string {
|
||||
const forwarded = req.headers['x-forwarded-for'];
|
||||
if (typeof forwarded === 'string') {
|
||||
return forwarded.split(',')[0].trim();
|
||||
}
|
||||
return req.socket.remoteAddress || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit Middleware - loggt alle API-Aufrufe asynchron
|
||||
*/
|
||||
export function auditMiddleware(req: AuthRequest, res: Response, next: NextFunction): void {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Ausgeschlossene Routen überspringen
|
||||
if (EXCLUDED_ROUTES.some((route) => req.path.startsWith(route))) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Resource-Mapping finden
|
||||
const mapping = findResourceMapping(req.path);
|
||||
if (!mapping) {
|
||||
// Unbekannte Route - trotzdem loggen mit generischem Typ
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Original res.json überschreiben um Response zu capturen
|
||||
const originalJson = res.json.bind(res);
|
||||
let responseBody: unknown = null;
|
||||
let responseSuccess = true;
|
||||
|
||||
res.json = function (body: unknown) {
|
||||
responseBody = body;
|
||||
if (typeof body === 'object' && body !== null && 'success' in body) {
|
||||
responseSuccess = (body as { success: boolean }).success;
|
||||
}
|
||||
return originalJson(body);
|
||||
};
|
||||
|
||||
// Response-Ende abfangen für Logging
|
||||
res.on('finish', () => {
|
||||
// Async Logging - blockiert nicht die Response
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
const durationMs = Date.now() - startTime;
|
||||
const action = determineAction(req.method, req.path, responseSuccess);
|
||||
const resourceId = mapping.extractId?.(req);
|
||||
const dataSubjectId = extractDataSubjectId(req);
|
||||
|
||||
// Audit-Kontext abrufen (enthält Before/After-Werte von Prisma Middleware)
|
||||
const auditContext = getAuditContext();
|
||||
|
||||
// Label für bessere Lesbarkeit generieren
|
||||
let resourceLabel: string | undefined;
|
||||
if (responseBody && typeof responseBody === 'object' && 'data' in responseBody) {
|
||||
const data = (responseBody as { data: Record<string, unknown> }).data;
|
||||
if (data) {
|
||||
// Versuche verschiedene Label-Felder
|
||||
resourceLabel =
|
||||
(data.contractNumber as string) ||
|
||||
(data.customerNumber as string) ||
|
||||
(data.name as string) ||
|
||||
(data.email as string) ||
|
||||
(data.firstName && data.lastName
|
||||
? `${data.firstName} ${data.lastName}`
|
||||
: undefined);
|
||||
}
|
||||
}
|
||||
|
||||
await createAuditLog({
|
||||
userId: req.user?.userId,
|
||||
userEmail: req.user?.email || 'anonymous',
|
||||
userRole: req.user?.permissions?.join(', '),
|
||||
customerId: req.user?.customerId,
|
||||
isCustomerPortal: req.user?.isCustomerPortal,
|
||||
action,
|
||||
resourceType: mapping.type,
|
||||
resourceId,
|
||||
resourceLabel,
|
||||
endpoint: req.path,
|
||||
httpMethod: req.method,
|
||||
ipAddress: getClientIp(req),
|
||||
userAgent: req.headers['user-agent'],
|
||||
changesBefore: auditContext?.before,
|
||||
changesAfter: auditContext?.after,
|
||||
dataSubjectId,
|
||||
success: responseSuccess,
|
||||
errorMessage: !responseSuccess && responseBody && typeof responseBody === 'object' && 'error' in responseBody
|
||||
? (responseBody as { error: string }).error
|
||||
: undefined,
|
||||
durationMs,
|
||||
});
|
||||
} catch (error) {
|
||||
// Audit-Logging darf niemals die Anwendung beeinträchtigen
|
||||
console.error('[AuditMiddleware] Fehler beim Logging:', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { AsyncLocalStorage } from 'async_hooks';
|
||||
|
||||
/**
|
||||
* Audit-Kontext für die Übertragung von Before/After-Werten
|
||||
* zwischen Prisma-Middleware und Audit-Middleware
|
||||
*/
|
||||
export interface AuditContext {
|
||||
before?: Record<string, unknown>;
|
||||
after?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// AsyncLocalStorage für den Audit-Kontext
|
||||
const auditStorage = new AsyncLocalStorage<AuditContext>();
|
||||
|
||||
/**
|
||||
* Startet einen neuen Audit-Kontext für einen Request
|
||||
*/
|
||||
export function runWithAuditContext<T>(fn: () => T): T {
|
||||
return auditStorage.run({}, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die "Before"-Werte im aktuellen Kontext
|
||||
*/
|
||||
export function setBeforeValues(values: Record<string, unknown>): void {
|
||||
const context = auditStorage.getStore();
|
||||
if (context) {
|
||||
context.before = values;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die "After"-Werte im aktuellen Kontext
|
||||
*/
|
||||
export function setAfterValues(values: Record<string, unknown>): void {
|
||||
const context = auditStorage.getStore();
|
||||
if (context) {
|
||||
context.after = values;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt den aktuellen Audit-Kontext
|
||||
*/
|
||||
export function getAuditContext(): AuditContext | undefined {
|
||||
return auditStorage.getStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Express Middleware zum Initialisieren des Audit-Kontexts
|
||||
*/
|
||||
export function auditContextMiddleware(
|
||||
req: unknown,
|
||||
res: unknown,
|
||||
next: () => void
|
||||
): void {
|
||||
runWithAuditContext(() => {
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
|
@ -114,9 +114,14 @@ export function requireCustomerAccess(
|
|||
return;
|
||||
}
|
||||
|
||||
// Customers can only access their own data
|
||||
// Customers can only access their own data + represented customers
|
||||
const customerId = parseInt(req.params.customerId || req.params.id);
|
||||
if (req.user.customerId && req.user.customerId === customerId) {
|
||||
const allowedIds = [
|
||||
req.user.customerId,
|
||||
...((req.user as any).representedCustomerIds || []),
|
||||
].filter(Boolean);
|
||||
|
||||
if (allowedIds.includes(customerId)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import { Router } from 'express';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
import * as auditLogController from '../controllers/auditLog.controller.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Alle Routen erfordern Authentifizierung
|
||||
router.use(authenticate);
|
||||
|
||||
// Audit-Logs abrufen
|
||||
router.get('/', requirePermission('audit:read'), auditLogController.getAuditLogs);
|
||||
|
||||
// Einzelnes Audit-Log abrufen
|
||||
router.get('/:id', requirePermission('audit:read'), auditLogController.getAuditLogById);
|
||||
|
||||
// Audit-Logs für einen Kunden (DSGVO)
|
||||
router.get('/customer/:customerId', requirePermission('audit:read'), auditLogController.getAuditLogsByCustomer);
|
||||
|
||||
// Audit-Logs exportieren
|
||||
router.get('/export', requirePermission('audit:export'), auditLogController.exportAuditLogs);
|
||||
|
||||
// Hash-Ketten-Integrität prüfen
|
||||
router.post('/verify', requirePermission('audit:admin'), auditLogController.verifyIntegrity);
|
||||
|
||||
// Retention-Policies
|
||||
router.get('/retention-policies', requirePermission('audit:admin'), auditLogController.getRetentionPolicies);
|
||||
router.put('/retention-policies/:id', requirePermission('audit:admin'), auditLogController.updateRetentionPolicy);
|
||||
|
||||
// Retention-Cleanup manuell ausführen
|
||||
router.post('/cleanup', requirePermission('audit:admin'), auditLogController.runRetentionCleanup);
|
||||
|
||||
export default router;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Router } from 'express';
|
||||
import * as controller from '../controllers/consent-public.controller.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Öffentliche Routes - KEINE Authentifizierung erforderlich
|
||||
router.get('/:hash', controller.getConsentPage);
|
||||
router.post('/:hash/grant', controller.grantAllConsents);
|
||||
router.get('/:hash/pdf', controller.getConsentPdf);
|
||||
|
||||
export default router;
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Router } from 'express';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
import * as emailLogController from '../controllers/emailLog.controller.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
router.get('/', requirePermission('gdpr:admin'), emailLogController.getEmailLogs);
|
||||
router.get('/stats', requirePermission('gdpr:admin'), emailLogController.getEmailLogStats);
|
||||
router.get('/:id', requirePermission('gdpr:admin'), emailLogController.getEmailLogDetail);
|
||||
|
||||
export default router;
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
import * as gdprController from '../controllers/gdpr.controller.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Multer für Vollmacht-Uploads
|
||||
const uploadsDir = path.join(process.cwd(), 'uploads', 'authorizations');
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
}
|
||||
const authUpload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (_req, _file, cb) => cb(null, uploadsDir),
|
||||
filename: (_req, file, cb) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
cb(null, `vollmacht-${uniqueSuffix}${path.extname(file.originalname)}`);
|
||||
},
|
||||
}),
|
||||
fileFilter: (_req, file, cb) => {
|
||||
if (file.mimetype === 'application/pdf') {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Nur PDF-Dateien sind erlaubt'));
|
||||
}
|
||||
},
|
||||
limits: { fileSize: 10 * 1024 * 1024 },
|
||||
});
|
||||
|
||||
// Alle Routen erfordern Authentifizierung
|
||||
router.use(authenticate);
|
||||
|
||||
// Dashboard-Statistiken
|
||||
router.get('/dashboard', requirePermission('gdpr:admin'), gdprController.getDashboardStats);
|
||||
|
||||
// Kundendaten exportieren (DSGVO Art. 15)
|
||||
router.get('/customer/:customerId/export', requirePermission('gdpr:export'), gdprController.exportCustomerData);
|
||||
|
||||
// Löschanfragen
|
||||
router.get('/deletions', requirePermission('gdpr:admin'), gdprController.getDeletionRequests);
|
||||
router.get('/deletions/:id', requirePermission('gdpr:admin'), gdprController.getDeletionRequest);
|
||||
router.post('/deletions', requirePermission('gdpr:delete'), gdprController.createDeletionRequest);
|
||||
router.put('/deletions/:id/process', requirePermission('gdpr:admin'), gdprController.processDeletionRequest);
|
||||
|
||||
// Einwilligungen (Consents)
|
||||
router.get('/customer/:customerId/consent-status', requirePermission('customers:read'), gdprController.checkConsentStatus);
|
||||
router.get('/customer/:customerId/consents', requirePermission('customers:read'), gdprController.getCustomerConsents);
|
||||
// Consent-Update: Nur authenticate (Check im Controller - nur Portal-User erlaubt)
|
||||
router.put('/customer/:customerId/consents/:consentType', gdprController.updateCustomerConsent);
|
||||
router.get('/consents/overview', requirePermission('gdpr:admin'), gdprController.getConsentOverview);
|
||||
|
||||
// Datenschutzerklärung (Editor)
|
||||
router.get('/privacy-policy', requirePermission('gdpr:admin'), gdprController.getPrivacyPolicy);
|
||||
router.put('/privacy-policy', requirePermission('gdpr:admin'), gdprController.updatePrivacyPolicy);
|
||||
|
||||
// Vollmacht-Vorlage (Editor)
|
||||
router.get('/authorization-template', requirePermission('gdpr:admin'), gdprController.getAuthorizationTemplate);
|
||||
router.put('/authorization-template', requirePermission('gdpr:admin'), gdprController.updateAuthorizationTemplate);
|
||||
|
||||
// Consent-Link senden
|
||||
router.post('/customer/:customerId/send-consent-link', requirePermission('customers:update'), gdprController.sendConsentLink);
|
||||
|
||||
// Portal: Eigene Datenschutzseite (nur authenticate, Check im Controller)
|
||||
router.get('/my-privacy', gdprController.getMyPrivacy);
|
||||
router.get('/my-privacy/pdf', gdprController.getMyPrivacyPdf);
|
||||
router.get('/my-consent-status', gdprController.getMyConsentStatus);
|
||||
|
||||
// Vollmachten (Admin)
|
||||
router.get('/customer/:customerId/authorizations', requirePermission('customers:read'), gdprController.getAuthorizations);
|
||||
router.post('/customer/:customerId/authorizations/:representativeId/send', requirePermission('customers:update'), gdprController.sendAuthorizationRequest);
|
||||
router.post('/customer/:customerId/authorizations/:representativeId/grant', requirePermission('customers:update'), gdprController.grantAuthorization);
|
||||
router.post('/customer/:customerId/authorizations/:representativeId/withdraw', requirePermission('customers:update'), gdprController.withdrawAuthorization);
|
||||
router.post('/customer/:customerId/authorizations/:representativeId/upload', requirePermission('customers:update'), authUpload.single('document'), gdprController.uploadAuthorizationDocument);
|
||||
router.delete('/customer/:customerId/authorizations/:representativeId/document', requirePermission('customers:update'), gdprController.deleteAuthorizationDocument);
|
||||
|
||||
// Portal: Vollmachten
|
||||
router.get('/my-authorizations', gdprController.getMyAuthorizations);
|
||||
router.put('/my-authorizations/:representativeId', gdprController.toggleMyAuthorization);
|
||||
router.get('/my-authorization-status', gdprController.getMyAuthorizationStatus);
|
||||
|
||||
export default router;
|
||||
|
|
@ -13,4 +13,13 @@ router.post('/:meterId/readings', authenticate, requirePermission('customers:upd
|
|||
router.put('/:meterId/readings/:readingId', authenticate, requirePermission('customers:update'), customerController.updateMeterReading);
|
||||
router.delete('/:meterId/readings/:readingId', authenticate, requirePermission('customers:delete'), customerController.deleteMeterReading);
|
||||
|
||||
// Status-Update (Zählerstand als übertragen markieren)
|
||||
router.patch('/:meterId/readings/:readingId/transfer', authenticate, requirePermission('customers:update'), customerController.markReadingTransferred);
|
||||
|
||||
// Portal: Zählerstand melden (Kunde)
|
||||
router.post('/:meterId/readings/report', authenticate, customerController.reportMeterReading);
|
||||
|
||||
// Portal: Eigene Zähler laden
|
||||
router.get('/my-meters', authenticate, customerController.getMyMeters);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ const DEFAULT_SETTINGS: Record<string, string> = {
|
|||
deadlineCriticalDays: '14', // Rot: Kritisch
|
||||
deadlineWarningDays: '42', // Gelb: Warnung (6 Wochen)
|
||||
deadlineOkDays: '90', // Grün: OK (3 Monate)
|
||||
// Ausweis-Ablauf: Fristenschwellen (in Tagen)
|
||||
documentExpiryCriticalDays: '30', // Rot: Kritisch (Standard 30 Tage)
|
||||
documentExpiryWarningDays: '90', // Gelb: Warnung (Standard 90 Tage)
|
||||
};
|
||||
|
||||
export async function getSetting(key: string): Promise<string | null> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,504 @@
|
|||
import { AuditAction, AuditSensitivity, Prisma } from '@prisma/client';
|
||||
import crypto from 'crypto';
|
||||
import { encrypt, decrypt } from '../utils/encryption.js';
|
||||
import prisma from '../lib/prisma.js';
|
||||
|
||||
export interface CreateAuditLogData {
|
||||
userId?: number;
|
||||
userEmail: string;
|
||||
userRole?: string;
|
||||
customerId?: number;
|
||||
isCustomerPortal?: boolean;
|
||||
action: AuditAction;
|
||||
sensitivity?: AuditSensitivity;
|
||||
resourceType: string;
|
||||
resourceId?: string;
|
||||
resourceLabel?: string;
|
||||
endpoint: string;
|
||||
httpMethod: string;
|
||||
ipAddress: string;
|
||||
userAgent?: string;
|
||||
changesBefore?: Record<string, unknown>;
|
||||
changesAfter?: Record<string, unknown>;
|
||||
dataSubjectId?: number;
|
||||
legalBasis?: string;
|
||||
success?: boolean;
|
||||
errorMessage?: string;
|
||||
durationMs?: number;
|
||||
}
|
||||
|
||||
export interface AuditLogSearchParams {
|
||||
userId?: number;
|
||||
customerId?: number;
|
||||
dataSubjectId?: number;
|
||||
action?: AuditAction;
|
||||
sensitivity?: AuditSensitivity;
|
||||
resourceType?: string;
|
||||
resourceId?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
success?: boolean;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen SHA-256 Hash für einen Audit-Log-Eintrag
|
||||
*/
|
||||
function generateHash(data: {
|
||||
userEmail: string;
|
||||
action: AuditAction;
|
||||
resourceType: string;
|
||||
resourceId?: string | null;
|
||||
endpoint: string;
|
||||
createdAt: Date;
|
||||
previousHash?: string | null;
|
||||
}): string {
|
||||
const content = JSON.stringify({
|
||||
userEmail: data.userEmail,
|
||||
action: data.action,
|
||||
resourceType: data.resourceType,
|
||||
resourceId: data.resourceId,
|
||||
endpoint: data.endpoint,
|
||||
createdAt: data.createdAt.toISOString(),
|
||||
previousHash: data.previousHash || '',
|
||||
});
|
||||
return crypto.createHash('sha256').update(content).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt die Sensitivität basierend auf dem Ressourcentyp
|
||||
*/
|
||||
function determineSensitivity(resourceType: string): AuditSensitivity {
|
||||
const sensitivityMap: Record<string, AuditSensitivity> = {
|
||||
// CRITICAL
|
||||
Authentication: 'CRITICAL',
|
||||
BankCard: 'CRITICAL',
|
||||
IdentityDocument: 'CRITICAL',
|
||||
// HIGH
|
||||
Customer: 'HIGH',
|
||||
User: 'HIGH',
|
||||
CustomerConsent: 'HIGH',
|
||||
DataDeletionRequest: 'HIGH',
|
||||
// MEDIUM
|
||||
Contract: 'MEDIUM',
|
||||
Address: 'MEDIUM',
|
||||
Meter: 'MEDIUM',
|
||||
MeterReading: 'MEDIUM',
|
||||
StressfreiEmail: 'MEDIUM',
|
||||
CachedEmail: 'MEDIUM',
|
||||
// LOW
|
||||
Provider: 'LOW',
|
||||
Tariff: 'LOW',
|
||||
SalesPlatform: 'LOW',
|
||||
AppSetting: 'LOW',
|
||||
ContractCategory: 'LOW',
|
||||
CancellationPeriod: 'LOW',
|
||||
ContractDuration: 'LOW',
|
||||
};
|
||||
return sensitivityMap[resourceType] || 'MEDIUM';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob Änderungen verschlüsselt werden sollen
|
||||
*/
|
||||
function shouldEncryptChanges(resourceType: string): boolean {
|
||||
const encryptedTypes = [
|
||||
'BankCard',
|
||||
'IdentityDocument',
|
||||
'User',
|
||||
'Customer', // Enthält Portal-Passwörter
|
||||
];
|
||||
return encryptedTypes.includes(resourceType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Audit-Log-Eintrag mit Hash-Kette
|
||||
*/
|
||||
export async function createAuditLog(data: CreateAuditLogData): Promise<void> {
|
||||
try {
|
||||
// Letzten Hash abrufen für die Kette
|
||||
const lastLog = await prisma.auditLog.findFirst({
|
||||
orderBy: { id: 'desc' },
|
||||
select: { hash: true },
|
||||
});
|
||||
|
||||
const previousHash = lastLog?.hash || null;
|
||||
const createdAt = new Date();
|
||||
|
||||
// Sensitivität bestimmen falls nicht angegeben
|
||||
const sensitivity = data.sensitivity || determineSensitivity(data.resourceType);
|
||||
|
||||
// Änderungen serialisieren und ggf. verschlüsseln
|
||||
let changesBefore: string | null = null;
|
||||
let changesAfter: string | null = null;
|
||||
let changesEncrypted = false;
|
||||
|
||||
if (data.changesBefore || data.changesAfter) {
|
||||
changesEncrypted = shouldEncryptChanges(data.resourceType);
|
||||
|
||||
if (data.changesBefore) {
|
||||
const json = JSON.stringify(data.changesBefore);
|
||||
changesBefore = changesEncrypted ? encrypt(json) : json;
|
||||
}
|
||||
|
||||
if (data.changesAfter) {
|
||||
const json = JSON.stringify(data.changesAfter);
|
||||
changesAfter = changesEncrypted ? encrypt(json) : json;
|
||||
}
|
||||
}
|
||||
|
||||
// Hash generieren
|
||||
const hash = generateHash({
|
||||
userEmail: data.userEmail,
|
||||
action: data.action,
|
||||
resourceType: data.resourceType,
|
||||
resourceId: data.resourceId,
|
||||
endpoint: data.endpoint,
|
||||
createdAt,
|
||||
previousHash,
|
||||
});
|
||||
|
||||
// Eintrag erstellen
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId: data.userId,
|
||||
userEmail: data.userEmail,
|
||||
userRole: data.userRole,
|
||||
customerId: data.customerId,
|
||||
isCustomerPortal: data.isCustomerPortal || false,
|
||||
action: data.action,
|
||||
sensitivity,
|
||||
resourceType: data.resourceType,
|
||||
resourceId: data.resourceId,
|
||||
resourceLabel: data.resourceLabel,
|
||||
endpoint: data.endpoint,
|
||||
httpMethod: data.httpMethod,
|
||||
ipAddress: data.ipAddress,
|
||||
userAgent: data.userAgent,
|
||||
changesBefore,
|
||||
changesAfter,
|
||||
changesEncrypted,
|
||||
dataSubjectId: data.dataSubjectId,
|
||||
legalBasis: data.legalBasis,
|
||||
success: data.success ?? true,
|
||||
errorMessage: data.errorMessage,
|
||||
durationMs: data.durationMs,
|
||||
createdAt,
|
||||
hash,
|
||||
previousHash,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// Audit-Logging darf niemals die Hauptoperation blockieren
|
||||
console.error('[AuditService] Fehler beim Erstellen des Audit-Logs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht Audit-Logs mit Filtern und Paginierung
|
||||
*/
|
||||
export async function searchAuditLogs(params: AuditLogSearchParams) {
|
||||
const {
|
||||
userId,
|
||||
customerId,
|
||||
dataSubjectId,
|
||||
action,
|
||||
sensitivity,
|
||||
resourceType,
|
||||
resourceId,
|
||||
startDate,
|
||||
endDate,
|
||||
success,
|
||||
search,
|
||||
page = 1,
|
||||
limit = 50,
|
||||
} = params;
|
||||
|
||||
const where: Prisma.AuditLogWhereInput = {};
|
||||
|
||||
if (userId !== undefined) where.userId = userId;
|
||||
if (customerId !== undefined) where.customerId = customerId;
|
||||
if (dataSubjectId !== undefined) where.dataSubjectId = dataSubjectId;
|
||||
if (action) where.action = action;
|
||||
if (sensitivity) where.sensitivity = sensitivity;
|
||||
if (resourceType) where.resourceType = resourceType;
|
||||
if (resourceId) where.resourceId = resourceId;
|
||||
if (success !== undefined) where.success = success;
|
||||
|
||||
if (startDate || endDate) {
|
||||
where.createdAt = {};
|
||||
if (startDate) where.createdAt.gte = startDate;
|
||||
if (endDate) where.createdAt.lte = endDate;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ userEmail: { contains: search } },
|
||||
{ resourceLabel: { contains: search } },
|
||||
{ endpoint: { contains: search } },
|
||||
];
|
||||
}
|
||||
|
||||
const [logs, total] = await Promise.all([
|
||||
prisma.auditLog.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.auditLog.count({ where }),
|
||||
]);
|
||||
|
||||
// Entschlüsselung der Änderungen wenn nötig
|
||||
const decryptedLogs = logs.map((log) => ({
|
||||
...log,
|
||||
changesBefore: log.changesBefore && log.changesEncrypted
|
||||
? JSON.parse(decrypt(log.changesBefore))
|
||||
: log.changesBefore ? JSON.parse(log.changesBefore) : null,
|
||||
changesAfter: log.changesAfter && log.changesEncrypted
|
||||
? JSON.parse(decrypt(log.changesAfter))
|
||||
: log.changesAfter ? JSON.parse(log.changesAfter) : null,
|
||||
}));
|
||||
|
||||
return {
|
||||
data: decryptedLogs,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt einen einzelnen Audit-Log-Eintrag
|
||||
*/
|
||||
export async function getAuditLogById(id: number) {
|
||||
const log = await prisma.auditLog.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!log) return null;
|
||||
|
||||
return {
|
||||
...log,
|
||||
changesBefore: log.changesBefore && log.changesEncrypted
|
||||
? JSON.parse(decrypt(log.changesBefore))
|
||||
: log.changesBefore ? JSON.parse(log.changesBefore) : null,
|
||||
changesAfter: log.changesAfter && log.changesEncrypted
|
||||
? JSON.parse(decrypt(log.changesAfter))
|
||||
: log.changesAfter ? JSON.parse(log.changesAfter) : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt alle Audit-Logs für eine betroffene Person (DSGVO)
|
||||
*/
|
||||
export async function getAuditLogsByDataSubject(customerId: number) {
|
||||
const logs = await prisma.auditLog.findMany({
|
||||
where: { dataSubjectId: customerId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
return logs.map((log) => ({
|
||||
...log,
|
||||
changesBefore: log.changesBefore && log.changesEncrypted
|
||||
? JSON.parse(decrypt(log.changesBefore))
|
||||
: log.changesBefore ? JSON.parse(log.changesBefore) : null,
|
||||
changesAfter: log.changesAfter && log.changesEncrypted
|
||||
? JSON.parse(decrypt(log.changesAfter))
|
||||
: log.changesAfter ? JSON.parse(log.changesAfter) : null,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifiziert die Integrität der Hash-Kette
|
||||
*/
|
||||
export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
|
||||
valid: boolean;
|
||||
checkedCount: number;
|
||||
invalidEntries: number[];
|
||||
}> {
|
||||
const where: Prisma.AuditLogWhereInput = {};
|
||||
|
||||
if (fromId !== undefined) where.id = { gte: fromId };
|
||||
if (toId !== undefined) where.id = { ...(where.id as object || {}), lte: toId };
|
||||
|
||||
const logs = await prisma.auditLog.findMany({
|
||||
where,
|
||||
orderBy: { id: 'asc' },
|
||||
select: {
|
||||
id: true,
|
||||
userEmail: true,
|
||||
action: true,
|
||||
resourceType: true,
|
||||
resourceId: true,
|
||||
endpoint: true,
|
||||
createdAt: true,
|
||||
hash: true,
|
||||
previousHash: true,
|
||||
},
|
||||
});
|
||||
|
||||
const invalidEntries: number[] = [];
|
||||
|
||||
for (let i = 0; i < logs.length; i++) {
|
||||
const log = logs[i];
|
||||
|
||||
// Hash neu berechnen
|
||||
const expectedHash = generateHash({
|
||||
userEmail: log.userEmail,
|
||||
action: log.action,
|
||||
resourceType: log.resourceType,
|
||||
resourceId: log.resourceId,
|
||||
endpoint: log.endpoint,
|
||||
createdAt: log.createdAt,
|
||||
previousHash: log.previousHash,
|
||||
});
|
||||
|
||||
// Prüfen ob Hash übereinstimmt
|
||||
if (log.hash !== expectedHash) {
|
||||
invalidEntries.push(log.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prüfen ob previousHash mit dem Hash des vorherigen Eintrags übereinstimmt
|
||||
if (i > 0) {
|
||||
const previousLog = logs[i - 1];
|
||||
if (log.previousHash !== previousLog.hash) {
|
||||
invalidEntries.push(log.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: invalidEntries.length === 0,
|
||||
checkedCount: logs.length,
|
||||
invalidEntries,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Exportiert Audit-Logs als JSON oder CSV
|
||||
*/
|
||||
export async function exportAuditLogs(
|
||||
params: AuditLogSearchParams,
|
||||
format: 'json' | 'csv' = 'json'
|
||||
): Promise<string> {
|
||||
// Alle Logs ohne Paginierung
|
||||
const result = await searchAuditLogs({ ...params, limit: 100000, page: 1 });
|
||||
const logs = result.data;
|
||||
|
||||
if (format === 'json') {
|
||||
return JSON.stringify(logs, null, 2);
|
||||
}
|
||||
|
||||
// CSV Export
|
||||
const headers = [
|
||||
'ID',
|
||||
'Zeitstempel',
|
||||
'Benutzer',
|
||||
'Aktion',
|
||||
'Ressource',
|
||||
'Ressource-ID',
|
||||
'Bezeichnung',
|
||||
'Endpoint',
|
||||
'IP-Adresse',
|
||||
'Erfolg',
|
||||
'Sensitivität',
|
||||
];
|
||||
|
||||
const rows = logs.map((log) => [
|
||||
log.id.toString(),
|
||||
log.createdAt.toISOString(),
|
||||
log.userEmail,
|
||||
log.action,
|
||||
log.resourceType,
|
||||
log.resourceId || '',
|
||||
log.resourceLabel || '',
|
||||
log.endpoint,
|
||||
log.ipAddress,
|
||||
log.success ? 'Ja' : 'Nein',
|
||||
log.sensitivity,
|
||||
]);
|
||||
|
||||
const csvContent = [
|
||||
headers.join(';'),
|
||||
...rows.map((row) => row.map((cell) => `"${cell.replace(/"/g, '""')}"`).join(';')),
|
||||
].join('\n');
|
||||
|
||||
return csvContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht alte Audit-Logs basierend auf Retention-Policies
|
||||
* Hinweis: Diese Funktion sollte nur von einem autorisierten Admin-Prozess aufgerufen werden
|
||||
*/
|
||||
export async function runRetentionCleanup(): Promise<{
|
||||
deletedCount: number;
|
||||
policies: Array<{ resourceType: string; sensitivity: string | null; deletedCount: number }>;
|
||||
}> {
|
||||
const policies = await prisma.auditRetentionPolicy.findMany({
|
||||
where: { isActive: true },
|
||||
});
|
||||
|
||||
const results: Array<{ resourceType: string; sensitivity: string | null; deletedCount: number }> = [];
|
||||
let totalDeleted = 0;
|
||||
|
||||
for (const policy of policies) {
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - policy.retentionDays);
|
||||
|
||||
const where: Prisma.AuditLogWhereInput = {
|
||||
createdAt: { lt: cutoffDate },
|
||||
};
|
||||
|
||||
if (policy.resourceType !== '*') {
|
||||
where.resourceType = policy.resourceType;
|
||||
}
|
||||
|
||||
if (policy.sensitivity) {
|
||||
where.sensitivity = policy.sensitivity;
|
||||
}
|
||||
|
||||
const deleted = await prisma.auditLog.deleteMany({ where });
|
||||
|
||||
results.push({
|
||||
resourceType: policy.resourceType,
|
||||
sensitivity: policy.sensitivity,
|
||||
deletedCount: deleted.count,
|
||||
});
|
||||
|
||||
totalDeleted += deleted.count;
|
||||
}
|
||||
|
||||
return {
|
||||
deletedCount: totalDeleted,
|
||||
policies: results,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt die Retention-Policies
|
||||
*/
|
||||
export async function getRetentionPolicies() {
|
||||
return prisma.auditRetentionPolicy.findMany({
|
||||
orderBy: [{ resourceType: 'asc' }, { sensitivity: 'asc' }],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert eine Retention-Policy
|
||||
*/
|
||||
export async function updateRetentionPolicy(
|
||||
id: number,
|
||||
data: { retentionDays?: number; description?: string; legalBasis?: string; isActive?: boolean }
|
||||
) {
|
||||
return prisma.auditRetentionPolicy.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
|
@ -283,6 +283,9 @@ export async function getUserById(id: number) {
|
|||
lastName: user.lastName,
|
||||
isActive: user.isActive,
|
||||
customerId: user.customerId,
|
||||
whatsappNumber: user.whatsappNumber,
|
||||
telegramUsername: user.telegramUsername,
|
||||
signalNumber: user.signalNumber,
|
||||
roles: user.roles.map((ur) => ur.role.name),
|
||||
permissions: Array.from(permissions),
|
||||
isCustomerPortal: false,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,207 @@
|
|||
import prisma from '../lib/prisma.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Vollmachten für einen Kunden abrufen (wer darf diesen Kunden einsehen?)
|
||||
*/
|
||||
export async function getAuthorizationsForCustomer(customerId: number) {
|
||||
return prisma.representativeAuthorization.findMany({
|
||||
where: { customerId },
|
||||
include: {
|
||||
representative: {
|
||||
select: { id: true, customerNumber: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmachten die ein Vertreter erhalten hat (welche Kunden darf er einsehen?)
|
||||
*/
|
||||
export async function getAuthorizationsForRepresentative(representativeId: number) {
|
||||
return prisma.representativeAuthorization.findMany({
|
||||
where: { representativeId },
|
||||
include: {
|
||||
customer: {
|
||||
select: { id: true, customerNumber: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein Vertreter eine Vollmacht für einen Kunden hat
|
||||
*/
|
||||
export async function hasAuthorization(customerId: number, representativeId: number): Promise<boolean> {
|
||||
const auth = await prisma.representativeAuthorization.findUnique({
|
||||
where: {
|
||||
customerId_representativeId: { customerId, representativeId },
|
||||
},
|
||||
});
|
||||
|
||||
return auth?.isGranted === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmacht erteilen oder aktualisieren
|
||||
*/
|
||||
export async function grantAuthorization(
|
||||
customerId: number,
|
||||
representativeId: number,
|
||||
data: { source?: string; documentPath?: string; notes?: string }
|
||||
) {
|
||||
return prisma.representativeAuthorization.upsert({
|
||||
where: {
|
||||
customerId_representativeId: { customerId, representativeId },
|
||||
},
|
||||
update: {
|
||||
isGranted: true,
|
||||
grantedAt: new Date(),
|
||||
withdrawnAt: null,
|
||||
source: data.source,
|
||||
documentPath: data.documentPath ?? undefined,
|
||||
notes: data.notes ?? undefined,
|
||||
},
|
||||
create: {
|
||||
customerId,
|
||||
representativeId,
|
||||
isGranted: true,
|
||||
grantedAt: new Date(),
|
||||
source: data.source || 'crm-backend',
|
||||
documentPath: data.documentPath,
|
||||
notes: data.notes,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmacht widerrufen + PDF löschen falls vorhanden
|
||||
*/
|
||||
export async function withdrawAuthorization(customerId: number, representativeId: number) {
|
||||
// Erst prüfen ob eine PDF vorhanden ist
|
||||
const existing = await prisma.representativeAuthorization.findUnique({
|
||||
where: { customerId_representativeId: { customerId, representativeId } },
|
||||
select: { documentPath: true },
|
||||
});
|
||||
|
||||
// PDF vom Filesystem löschen
|
||||
if (existing?.documentPath) {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), existing.documentPath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Löschen der Vollmacht-PDF:', err);
|
||||
}
|
||||
}
|
||||
|
||||
return prisma.representativeAuthorization.update({
|
||||
where: {
|
||||
customerId_representativeId: { customerId, representativeId },
|
||||
},
|
||||
data: {
|
||||
isGranted: false,
|
||||
withdrawnAt: new Date(),
|
||||
documentPath: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmacht-Dokument (PDF) hochladen
|
||||
*/
|
||||
export async function updateAuthorizationDocument(
|
||||
customerId: number,
|
||||
representativeId: number,
|
||||
documentPath: string
|
||||
) {
|
||||
// Wenn Dokument hochgeladen wird, gilt das als Vollmacht erteilen
|
||||
return prisma.representativeAuthorization.upsert({
|
||||
where: {
|
||||
customerId_representativeId: { customerId, representativeId },
|
||||
},
|
||||
update: {
|
||||
documentPath,
|
||||
isGranted: true,
|
||||
grantedAt: new Date(),
|
||||
withdrawnAt: null,
|
||||
source: 'papier',
|
||||
},
|
||||
create: {
|
||||
customerId,
|
||||
representativeId,
|
||||
documentPath,
|
||||
isGranted: true,
|
||||
grantedAt: new Date(),
|
||||
source: 'papier',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmacht-Dokument löschen
|
||||
*/
|
||||
export async function deleteAuthorizationDocument(customerId: number, representativeId: number) {
|
||||
return prisma.representativeAuthorization.update({
|
||||
where: {
|
||||
customerId_representativeId: { customerId, representativeId },
|
||||
},
|
||||
data: {
|
||||
documentPath: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle genehmigten Vertreter-IDs für einen Kunden
|
||||
* (Welche Vertreter dürfen die Verträge dieses Kunden sehen?)
|
||||
*/
|
||||
export async function getAuthorizedRepresentativeIds(customerId: number): Promise<number[]> {
|
||||
const auths = await prisma.representativeAuthorization.findMany({
|
||||
where: { customerId, isGranted: true },
|
||||
select: { representativeId: true },
|
||||
});
|
||||
return auths.map((a) => a.representativeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Kunden-IDs für die ein Vertreter eine Vollmacht hat
|
||||
*/
|
||||
export async function getAuthorizedCustomerIds(representativeId: number): Promise<number[]> {
|
||||
const auths = await prisma.representativeAuthorization.findMany({
|
||||
where: { representativeId, isGranted: true },
|
||||
select: { customerId: true },
|
||||
});
|
||||
return auths.map((a) => a.customerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt fehlende Vollmacht-Einträge für bestehende Vertreterbeziehungen
|
||||
* (wird aufgerufen wenn man den Tab aufruft)
|
||||
*/
|
||||
export async function ensureAuthorizationEntries(customerId: number) {
|
||||
// Alle aktiven Vertreter für diesen Kunden
|
||||
const representatives = await prisma.customerRepresentative.findMany({
|
||||
where: { customerId, isActive: true },
|
||||
select: { representativeId: true },
|
||||
});
|
||||
|
||||
for (const rep of representatives) {
|
||||
// Erstelle Eintrag falls nicht vorhanden
|
||||
await prisma.representativeAuthorization.upsert({
|
||||
where: {
|
||||
customerId_representativeId: { customerId, representativeId: rep.representativeId },
|
||||
},
|
||||
update: {}, // Nichts ändern wenn schon vorhanden
|
||||
create: {
|
||||
customerId,
|
||||
representativeId: rep.representativeId,
|
||||
isGranted: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
import { ConsentType, ConsentStatus } from '@prisma/client';
|
||||
import crypto from 'crypto';
|
||||
import prisma from '../lib/prisma.js';
|
||||
import * as consentService from './consent.service.js';
|
||||
import * as appSettingService from './appSetting.service.js';
|
||||
import PDFDocument from 'pdfkit';
|
||||
|
||||
/**
|
||||
* Kunden-Lookup per consentHash
|
||||
*/
|
||||
export async function getCustomerByConsentHash(hash: string) {
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { consentHash: hash },
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
customerNumber: true,
|
||||
salutation: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!customer) return null;
|
||||
|
||||
const consents = await consentService.getCustomerConsents(customer.id);
|
||||
|
||||
return { customer, consents };
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle 4 Einwilligungen über den öffentlichen Link erteilen
|
||||
*/
|
||||
export async function grantAllConsentsPublic(hash: string, ipAddress: string) {
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { consentHash: hash },
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new Error('Ungültiger Link');
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const type of Object.values(ConsentType)) {
|
||||
const result = await consentService.updateConsent(customer.id, type, {
|
||||
status: ConsentStatus.GRANTED,
|
||||
source: 'public-link',
|
||||
ipAddress,
|
||||
createdBy: `${customer.firstName} ${customer.lastName} (Public-Link)`,
|
||||
});
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* consentHash generieren falls nicht vorhanden
|
||||
*/
|
||||
export async function ensureConsentHash(customerId: number): Promise<string> {
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
select: { consentHash: true },
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new Error('Kunde nicht gefunden');
|
||||
}
|
||||
|
||||
if (customer.consentHash) {
|
||||
return customer.consentHash;
|
||||
}
|
||||
|
||||
const hash = crypto.randomUUID();
|
||||
await prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: { consentHash: hash },
|
||||
});
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Platzhalter in Text ersetzen
|
||||
*/
|
||||
function replacePlaceholders(html: string, customer: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
customerNumber: string;
|
||||
salutation?: string | null;
|
||||
email?: string | null;
|
||||
}): string {
|
||||
return html
|
||||
.replace(/\{\{vorname\}\}/gi, customer.firstName || '')
|
||||
.replace(/\{\{nachname\}\}/gi, customer.lastName || '')
|
||||
.replace(/\{\{kundennummer\}\}/gi, customer.customerNumber || '')
|
||||
.replace(/\{\{anrede\}\}/gi, customer.salutation || '')
|
||||
.replace(/\{\{email\}\}/gi, customer.email || '')
|
||||
.replace(/\{\{datum\}\}/gi, new Date().toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Datenschutzerklärung als HTML abrufen (mit Platzhaltern ersetzt)
|
||||
*/
|
||||
export async function getPrivacyPolicyHtml(customerId?: number): Promise<string> {
|
||||
const html = await appSettingService.getSetting('privacyPolicyHtml');
|
||||
|
||||
if (!html) {
|
||||
return '<p>Keine Datenschutzerklärung hinterlegt.</p>';
|
||||
}
|
||||
|
||||
if (!customerId) return html;
|
||||
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
customerNumber: true,
|
||||
salutation: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!customer) return html;
|
||||
|
||||
return replacePlaceholders(html, customer);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML zu Plain-Text konvertieren (für PDF)
|
||||
*/
|
||||
function htmlToText(html: string): string {
|
||||
return html
|
||||
.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, '\n$1\n')
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/<\/p>/gi, '\n\n')
|
||||
.replace(/<li[^>]*>(.*?)<\/li>/gi, ' • $1\n')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Datenschutzerklärung als PDF generieren
|
||||
*/
|
||||
export async function generateConsentPdf(customerId: number): Promise<Buffer> {
|
||||
const html = await getPrivacyPolicyHtml(customerId);
|
||||
const text = htmlToText(html);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const doc = new PDFDocument({ size: 'A4', margin: 50 });
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
doc.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
doc.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
doc.on('error', reject);
|
||||
|
||||
// Titel
|
||||
doc.fontSize(18).font('Helvetica-Bold').text('Datenschutzerklärung', { align: 'center' });
|
||||
doc.moveDown(1);
|
||||
|
||||
// Datum
|
||||
doc.fontSize(10).font('Helvetica')
|
||||
.text(`Erstellt am: ${new Date().toLocaleDateString('de-DE')}`, { align: 'right' });
|
||||
doc.moveDown(1);
|
||||
|
||||
// Inhalt
|
||||
doc.fontSize(11).font('Helvetica').text(text, {
|
||||
align: 'left',
|
||||
lineGap: 4,
|
||||
});
|
||||
|
||||
doc.end();
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
import { ConsentType, ConsentStatus } from '@prisma/client';
|
||||
import prisma from '../lib/prisma.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export interface UpdateConsentData {
|
||||
status: ConsentStatus;
|
||||
source?: string;
|
||||
documentPath?: string;
|
||||
version?: string;
|
||||
ipAddress?: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt alle Einwilligungen eines Kunden
|
||||
*/
|
||||
export async function getCustomerConsents(customerId: number) {
|
||||
const consents = await prisma.customerConsent.findMany({
|
||||
where: { customerId },
|
||||
orderBy: { consentType: 'asc' },
|
||||
});
|
||||
|
||||
// Alle verfügbaren Consent-Typen mit Status
|
||||
const allTypes = Object.values(ConsentType);
|
||||
const consentMap = new Map(consents.map((c) => [c.consentType, c]));
|
||||
|
||||
return allTypes.map((type) => {
|
||||
const existing = consentMap.get(type);
|
||||
return existing || {
|
||||
id: null,
|
||||
customerId,
|
||||
consentType: type,
|
||||
status: 'PENDING' as ConsentStatus,
|
||||
grantedAt: null,
|
||||
withdrawnAt: null,
|
||||
source: null,
|
||||
documentPath: null,
|
||||
version: null,
|
||||
ipAddress: null,
|
||||
createdBy: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert oder erstellt eine Einwilligung
|
||||
*/
|
||||
export async function updateConsent(
|
||||
customerId: number,
|
||||
consentType: ConsentType,
|
||||
data: UpdateConsentData
|
||||
) {
|
||||
// Prüfen ob Kunde existiert
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new Error('Kunde nicht gefunden');
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const updateData = {
|
||||
status: data.status,
|
||||
source: data.source,
|
||||
documentPath: data.documentPath,
|
||||
version: data.version,
|
||||
ipAddress: data.ipAddress,
|
||||
grantedAt: data.status === 'GRANTED' ? now : undefined,
|
||||
withdrawnAt: data.status === 'WITHDRAWN' ? now : undefined,
|
||||
};
|
||||
|
||||
const result = await prisma.customerConsent.upsert({
|
||||
where: {
|
||||
customerId_consentType: { customerId, consentType },
|
||||
},
|
||||
update: updateData,
|
||||
create: {
|
||||
customerId,
|
||||
consentType,
|
||||
...updateData,
|
||||
createdBy: data.createdBy,
|
||||
},
|
||||
});
|
||||
|
||||
// Bei Widerruf: Datenschutz-PDF löschen wenn keine Einwilligung mehr besteht
|
||||
if (data.status === 'WITHDRAWN') {
|
||||
await deletePrivacyPdfOnWithdraw(customerId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt die Historie einer Einwilligung (aus Audit-Logs)
|
||||
*/
|
||||
export async function getConsentHistory(customerId: number, consentType: ConsentType) {
|
||||
// Aus Audit-Logs die Änderungen dieser Einwilligung abrufen
|
||||
const logs = await prisma.auditLog.findMany({
|
||||
where: {
|
||||
resourceType: 'CustomerConsent',
|
||||
dataSubjectId: customerId,
|
||||
changesAfter: { contains: consentType },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50,
|
||||
});
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob eine bestimmte Einwilligung erteilt wurde
|
||||
*/
|
||||
export async function hasConsent(customerId: number, consentType: ConsentType): Promise<boolean> {
|
||||
const consent = await prisma.customerConsent.findUnique({
|
||||
where: {
|
||||
customerId_consentType: { customerId, consentType },
|
||||
},
|
||||
});
|
||||
|
||||
return consent?.status === 'GRANTED';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein Kunde die DSGVO-Einwilligung erfüllt hat.
|
||||
* Erfüllt = entweder privacyPolicyPath vorhanden ODER alle Online-Consents GRANTED.
|
||||
*/
|
||||
export async function hasFullConsent(customerId: number): Promise<{
|
||||
hasConsent: boolean;
|
||||
hasPaperConsent: boolean;
|
||||
hasOnlineConsent: boolean;
|
||||
consentDetails: { type: string; status: string }[];
|
||||
consentHash: string | null;
|
||||
}> {
|
||||
// Prüfe ob Papier-Datenschutzerklärung vorhanden
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
select: { privacyPolicyPath: true, consentHash: true },
|
||||
});
|
||||
|
||||
const hasPaperConsent = !!customer?.privacyPolicyPath;
|
||||
|
||||
// Online-Consents prüfen
|
||||
const allTypes = Object.values(ConsentType);
|
||||
const consents = await prisma.customerConsent.findMany({
|
||||
where: { customerId },
|
||||
});
|
||||
|
||||
const consentMap = new Map(consents.map((c) => [c.consentType, c.status]));
|
||||
const consentDetails = allTypes.map((type) => ({
|
||||
type,
|
||||
status: (consentMap.get(type) || 'PENDING') as string,
|
||||
}));
|
||||
|
||||
const hasOnlineConsent = allTypes.every(
|
||||
(type) => consentMap.get(type) === 'GRANTED'
|
||||
);
|
||||
|
||||
return {
|
||||
hasConsent: hasPaperConsent || hasOnlineConsent,
|
||||
hasPaperConsent,
|
||||
hasOnlineConsent,
|
||||
consentDetails,
|
||||
consentHash: customer?.consentHash || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Widerruft alle Einwilligungen eines Kunden
|
||||
*/
|
||||
export async function withdrawAllConsents(customerId: number, withdrawnBy: string) {
|
||||
const result = await prisma.customerConsent.updateMany({
|
||||
where: {
|
||||
customerId,
|
||||
status: 'GRANTED',
|
||||
},
|
||||
data: {
|
||||
status: 'WITHDRAWN',
|
||||
withdrawnAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// Datenschutz-PDF löschen
|
||||
await deletePrivacyPdfOnWithdraw(customerId);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht die Datenschutz-PDF bei Widerruf.
|
||||
* Sobald auch nur eine Einwilligung widerrufen wird, ist die Gesamteinwilligung ungültig.
|
||||
*/
|
||||
async function deletePrivacyPdfOnWithdraw(customerId: number) {
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
select: { privacyPolicyPath: true },
|
||||
});
|
||||
|
||||
if (customer?.privacyPolicyPath) {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), customer.privacyPolicyPath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Löschen der Datenschutz-PDF:', err);
|
||||
}
|
||||
|
||||
await prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: { privacyPolicyPath: null },
|
||||
});
|
||||
|
||||
console.log(`Datenschutz-PDF für Kunde ${customerId} gelöscht (Einwilligung widerrufen)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consent-Übersicht für DSGVO-Dashboard
|
||||
*/
|
||||
export async function getConsentOverview() {
|
||||
const allConsents = await prisma.customerConsent.groupBy({
|
||||
by: ['consentType', 'status'],
|
||||
_count: { id: true },
|
||||
});
|
||||
|
||||
// Gruppieren nach Typ
|
||||
const overview: Record<string, { granted: number; withdrawn: number; pending: number }> = {};
|
||||
|
||||
for (const type of Object.values(ConsentType)) {
|
||||
overview[type] = { granted: 0, withdrawn: 0, pending: 0 };
|
||||
}
|
||||
|
||||
for (const row of allConsents) {
|
||||
const type = row.consentType;
|
||||
const status = row.status.toLowerCase() as 'granted' | 'withdrawn' | 'pending';
|
||||
overview[type][status] = row._count.id;
|
||||
}
|
||||
|
||||
return overview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consent-Typ Labels für UI
|
||||
*/
|
||||
export const CONSENT_TYPE_LABELS: Record<ConsentType, { label: string; description: string }> = {
|
||||
DATA_PROCESSING: {
|
||||
label: 'Datenverarbeitung',
|
||||
description: 'Grundlegende Verarbeitung personenbezogener Daten zur Vertragserfüllung',
|
||||
},
|
||||
MARKETING_EMAIL: {
|
||||
label: 'E-Mail-Marketing',
|
||||
description: 'Zusendung von Werbung und Angeboten per E-Mail',
|
||||
},
|
||||
MARKETING_PHONE: {
|
||||
label: 'Telefonmarketing',
|
||||
description: 'Kontaktaufnahme zu Werbezwecken per Telefon',
|
||||
},
|
||||
DATA_SHARING_PARTNER: {
|
||||
label: 'Datenweitergabe',
|
||||
description: 'Weitergabe von Daten an Partnerunternehmen',
|
||||
},
|
||||
};
|
||||
|
|
@ -53,11 +53,54 @@ export interface CockpitSummary {
|
|||
openTasks: number;
|
||||
pendingContracts: number;
|
||||
reviewDue: number; // Erneute Prüfung fällig (Snooze abgelaufen)
|
||||
missingConsents: number; // Fehlende oder widerrufene Einwilligungen
|
||||
};
|
||||
}
|
||||
|
||||
export interface DocumentAlert {
|
||||
id: number;
|
||||
type: string; // ID_CARD, PASSPORT, DRIVERS_LICENSE, OTHER
|
||||
documentNumber: string;
|
||||
expiryDate: string;
|
||||
daysUntilExpiry: number;
|
||||
urgency: UrgencyLevel;
|
||||
customer: {
|
||||
id: number;
|
||||
customerNumber: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReportedMeterReading {
|
||||
id: number;
|
||||
readingDate: string;
|
||||
value: number;
|
||||
unit: string;
|
||||
notes?: string;
|
||||
reportedBy?: string;
|
||||
createdAt: string;
|
||||
meter: {
|
||||
id: number;
|
||||
meterNumber: string;
|
||||
type: string;
|
||||
};
|
||||
customer: {
|
||||
id: number;
|
||||
customerNumber: string;
|
||||
name: string;
|
||||
};
|
||||
// Anbieter-Info für Quick-Login
|
||||
providerPortal?: {
|
||||
providerName: string;
|
||||
portalUrl: string;
|
||||
portalUsername?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CockpitResult {
|
||||
contracts: CockpitContract[];
|
||||
documentAlerts: DocumentAlert[];
|
||||
reportedReadings: ReportedMeterReading[];
|
||||
summary: CockpitSummary;
|
||||
thresholds: {
|
||||
criticalDays: number;
|
||||
|
|
@ -143,6 +186,8 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
|||
const criticalDays = parseInt(settings.deadlineCriticalDays) || 14;
|
||||
const warningDays = parseInt(settings.deadlineWarningDays) || 42;
|
||||
const okDays = parseInt(settings.deadlineOkDays) || 90;
|
||||
const docExpiryCriticalDays = parseInt(settings.documentExpiryCriticalDays) || 30;
|
||||
const docExpiryWarningDays = parseInt(settings.documentExpiryWarningDays) || 90;
|
||||
|
||||
// Lade alle relevanten Verträge (inkl. CANCELLED/DEACTIVATED für Schlussrechnung-Check)
|
||||
const contracts = await prisma.contract.findMany({
|
||||
|
|
@ -231,9 +276,41 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
|||
openTasks: 0,
|
||||
pendingContracts: 0,
|
||||
reviewDue: 0,
|
||||
missingConsents: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// Consent-Daten batch-laden für alle Kunden
|
||||
const allConsents = await prisma.customerConsent.findMany({
|
||||
where: { status: 'GRANTED' },
|
||||
select: { customerId: true, consentType: true },
|
||||
});
|
||||
|
||||
// Map: customerId → Set<consentType>
|
||||
const grantedConsentsMap = new Map<number, Set<string>>();
|
||||
for (const c of allConsents) {
|
||||
if (!grantedConsentsMap.has(c.customerId)) {
|
||||
grantedConsentsMap.set(c.customerId, new Set());
|
||||
}
|
||||
grantedConsentsMap.get(c.customerId)!.add(c.consentType);
|
||||
}
|
||||
|
||||
// Widerrufene Consents laden
|
||||
const withdrawnConsents = await prisma.customerConsent.findMany({
|
||||
where: { status: 'WITHDRAWN' },
|
||||
select: { customerId: true, consentType: true },
|
||||
});
|
||||
const withdrawnConsentsMap = new Map<number, Set<string>>();
|
||||
for (const c of withdrawnConsents) {
|
||||
if (!withdrawnConsentsMap.has(c.customerId)) {
|
||||
withdrawnConsentsMap.set(c.customerId, new Set());
|
||||
}
|
||||
withdrawnConsentsMap.get(c.customerId)!.add(c.consentType);
|
||||
}
|
||||
|
||||
// Track welche Kunden bereits eine Consent-Warnung bekommen haben (nur einmal pro Kunde)
|
||||
const customerConsentWarned = new Set<number>();
|
||||
|
||||
for (const contract of contracts) {
|
||||
const issues: CockpitIssue[] = [];
|
||||
|
||||
|
|
@ -407,17 +484,43 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
|||
summary.byCategory.missingData++;
|
||||
}
|
||||
|
||||
// 7b. KEIN AUSWEIS (für DSL, FIBER, CABLE, MOBILE ist dies ein kritisches Problem)
|
||||
if (!contract.identityDocumentId) {
|
||||
// 7b. KEIN AUSWEIS (nur für Telekommunikationsprodukte relevant)
|
||||
const requiresIdentityDocument = ['DSL', 'FIBER', 'CABLE', 'MOBILE'].includes(contract.type);
|
||||
if (requiresIdentityDocument && !contract.identityDocumentId) {
|
||||
issues.push({
|
||||
type: 'missing_identity_document',
|
||||
label: 'Ausweis fehlt',
|
||||
urgency: requiresBankAndId ? 'critical' : 'warning',
|
||||
urgency: 'critical',
|
||||
details: 'Kein Ausweisdokument verknüpft',
|
||||
});
|
||||
summary.byCategory.missingData++;
|
||||
}
|
||||
|
||||
// 7c. AUSWEIS LÄUFT AB (nur aktive Ausweise prüfen)
|
||||
if (contract.identityDocument && contract.identityDocument.isActive && contract.identityDocument.expiryDate) {
|
||||
const expiryDate = new Date(contract.identityDocument.expiryDate);
|
||||
const today = new Date();
|
||||
const daysUntilExpiry = Math.ceil((expiryDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysUntilExpiry < 0) {
|
||||
issues.push({
|
||||
type: 'identity_document_expired',
|
||||
label: 'Ausweis abgelaufen',
|
||||
urgency: 'critical',
|
||||
details: `Ausweis seit ${Math.abs(daysUntilExpiry)} Tagen abgelaufen (${expiryDate.toLocaleDateString('de-DE')})`,
|
||||
});
|
||||
summary.byCategory.missingData++;
|
||||
} else if (daysUntilExpiry <= docExpiryWarningDays) {
|
||||
issues.push({
|
||||
type: 'identity_document_expiring',
|
||||
label: 'Ausweis läuft ab',
|
||||
urgency: daysUntilExpiry <= docExpiryCriticalDays ? 'critical' : 'warning',
|
||||
details: `Ausweis läuft in ${daysUntilExpiry} Tagen ab (${expiryDate.toLocaleDateString('de-DE')})`,
|
||||
});
|
||||
summary.byCategory.cancellationDeadlines++;
|
||||
}
|
||||
}
|
||||
|
||||
// 8. ENERGIE-SPEZIFISCH: KEIN ZÄHLER
|
||||
if (['ELECTRICITY', 'GAS'].includes(contract.type) && contract.energyDetails) {
|
||||
if (!contract.energyDetails.meterId) {
|
||||
|
|
@ -546,6 +649,36 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
|||
}
|
||||
}
|
||||
|
||||
// #14 - Consent-Prüfung (nur für aktive Verträge, einmal pro Kunde)
|
||||
if (['ACTIVE', 'PENDING', 'DRAFT'].includes(contract.status) && !customerConsentWarned.has(contract.customer.id)) {
|
||||
const granted = grantedConsentsMap.get(contract.customer.id);
|
||||
const withdrawn = withdrawnConsentsMap.get(contract.customer.id);
|
||||
const requiredTypes = ['DATA_PROCESSING', 'MARKETING_EMAIL', 'MARKETING_PHONE', 'DATA_SHARING_PARTNER'];
|
||||
|
||||
if (withdrawn && withdrawn.size > 0) {
|
||||
// Mindestens eine Einwilligung widerrufen
|
||||
issues.push({
|
||||
type: 'consent_withdrawn',
|
||||
label: 'Einwilligung widerrufen',
|
||||
urgency: 'critical',
|
||||
details: `${withdrawn.size} Einwilligung(en) widerrufen`,
|
||||
});
|
||||
summary.byCategory.missingConsents++;
|
||||
customerConsentWarned.add(contract.customer.id);
|
||||
} else if (!granted || granted.size < requiredTypes.length) {
|
||||
// Nicht alle 4 Einwilligungen erteilt
|
||||
const missing = requiredTypes.length - (granted?.size || 0);
|
||||
issues.push({
|
||||
type: 'missing_consents',
|
||||
label: 'Fehlende Einwilligungen',
|
||||
urgency: 'critical',
|
||||
details: `${missing} von ${requiredTypes.length} Einwilligungen fehlen`,
|
||||
});
|
||||
summary.byCategory.missingConsents++;
|
||||
customerConsentWarned.add(contract.customer.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Nur Verträge mit Issues hinzufügen
|
||||
if (issues.length > 0) {
|
||||
const highestUrgency = getHighestUrgency(issues);
|
||||
|
|
@ -596,8 +729,16 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
|||
return urgencyOrder[a.highestUrgency] - urgencyOrder[b.highestUrgency];
|
||||
});
|
||||
|
||||
// Vertragsunabhängige Ausweis-Warnungen
|
||||
const documentAlerts = await getDocumentExpiryAlerts(docExpiryCriticalDays, docExpiryWarningDays);
|
||||
|
||||
// Gemeldete Zählerstände (REPORTED Status)
|
||||
const reportedReadings = await getReportedMeterReadings();
|
||||
|
||||
return {
|
||||
contracts: cockpitContracts,
|
||||
documentAlerts,
|
||||
reportedReadings,
|
||||
summary,
|
||||
thresholds: {
|
||||
criticalDays,
|
||||
|
|
@ -606,3 +747,111 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle aktiven Ausweise die ablaufen oder abgelaufen sind (vertragsunabhängig)
|
||||
*/
|
||||
async function getDocumentExpiryAlerts(criticalDays: number, warningDays: number): Promise<DocumentAlert[]> {
|
||||
const now = new Date();
|
||||
const inWarningDays = new Date(now.getTime() + warningDays * 24 * 60 * 60 * 1000);
|
||||
|
||||
const documents = await prisma.identityDocument.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
expiryDate: { lte: inWarningDays },
|
||||
},
|
||||
include: {
|
||||
customer: {
|
||||
select: { id: true, customerNumber: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
orderBy: { expiryDate: 'asc' },
|
||||
});
|
||||
|
||||
return documents.map((doc) => {
|
||||
const expiryDate = new Date(doc.expiryDate!);
|
||||
const daysUntilExpiry = Math.ceil((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
let urgency: UrgencyLevel = 'warning';
|
||||
if (daysUntilExpiry < 0) urgency = 'critical';
|
||||
else if (daysUntilExpiry <= criticalDays) urgency = 'critical';
|
||||
|
||||
return {
|
||||
id: doc.id,
|
||||
type: doc.type,
|
||||
documentNumber: doc.documentNumber,
|
||||
expiryDate: expiryDate.toISOString(),
|
||||
daysUntilExpiry,
|
||||
urgency,
|
||||
customer: {
|
||||
id: doc.customer.id,
|
||||
customerNumber: doc.customer.customerNumber,
|
||||
name: `${doc.customer.firstName} ${doc.customer.lastName}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vom Kunden gemeldete Zählerstände die noch nicht übertragen wurden
|
||||
*/
|
||||
async function getReportedMeterReadings(): Promise<ReportedMeterReading[]> {
|
||||
const readings = await prisma.meterReading.findMany({
|
||||
where: { status: 'REPORTED' },
|
||||
include: {
|
||||
meter: {
|
||||
include: {
|
||||
customer: {
|
||||
select: { id: true, customerNumber: true, firstName: true, lastName: true },
|
||||
},
|
||||
// Energie-Verträge für diesen Zähler (um Provider-Portal-Daten zu bekommen)
|
||||
energyDetails: {
|
||||
include: {
|
||||
contract: {
|
||||
select: {
|
||||
id: true,
|
||||
portalUsername: true,
|
||||
provider: {
|
||||
select: { id: true, name: true, portalUrl: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
|
||||
return readings.map((r) => {
|
||||
const contract = r.meter.energyDetails?.[0]?.contract;
|
||||
const provider = contract?.provider;
|
||||
|
||||
return {
|
||||
id: r.id,
|
||||
readingDate: r.readingDate.toISOString(),
|
||||
value: r.value,
|
||||
unit: r.unit,
|
||||
notes: r.notes ?? undefined,
|
||||
reportedBy: r.reportedBy ?? undefined,
|
||||
createdAt: r.createdAt.toISOString(),
|
||||
meter: {
|
||||
id: r.meter.id,
|
||||
meterNumber: r.meter.meterNumber,
|
||||
type: r.meter.type,
|
||||
},
|
||||
customer: {
|
||||
id: r.meter.customer.id,
|
||||
customerNumber: r.meter.customer.customerNumber,
|
||||
name: `${r.meter.customer.firstName} ${r.meter.customer.lastName}`,
|
||||
},
|
||||
providerPortal: provider?.portalUrl ? {
|
||||
providerName: provider.name,
|
||||
portalUrl: provider.portalUrl,
|
||||
portalUsername: contract?.portalUsername ?? undefined,
|
||||
} : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
import prisma from '../lib/prisma.js';
|
||||
|
||||
export interface CreateEmailLogData {
|
||||
fromAddress: string;
|
||||
toAddress: string;
|
||||
subject: string;
|
||||
context: string;
|
||||
customerId?: number;
|
||||
triggeredBy?: string;
|
||||
smtpServer: string;
|
||||
smtpPort: number;
|
||||
smtpEncryption: string;
|
||||
smtpUser: string;
|
||||
success: boolean;
|
||||
messageId?: string;
|
||||
errorMessage?: string;
|
||||
smtpResponse?: string;
|
||||
}
|
||||
|
||||
export async function createEmailLog(data: CreateEmailLogData) {
|
||||
return prisma.emailLog.create({ data });
|
||||
}
|
||||
|
||||
export async function getEmailLogs(options?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
success?: boolean;
|
||||
search?: string;
|
||||
context?: string;
|
||||
}) {
|
||||
const page = options?.page || 1;
|
||||
const limit = options?.limit || 50;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: Record<string, unknown> = {};
|
||||
|
||||
if (options?.success !== undefined) {
|
||||
where.success = options.success;
|
||||
}
|
||||
|
||||
if (options?.context) {
|
||||
where.context = options.context;
|
||||
}
|
||||
|
||||
if (options?.search) {
|
||||
where.OR = [
|
||||
{ fromAddress: { contains: options.search } },
|
||||
{ toAddress: { contains: options.search } },
|
||||
{ subject: { contains: options.search } },
|
||||
{ errorMessage: { contains: options.search } },
|
||||
];
|
||||
}
|
||||
|
||||
const [logs, total] = await Promise.all([
|
||||
prisma.emailLog.findMany({
|
||||
where,
|
||||
orderBy: { sentAt: 'desc' },
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.emailLog.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
data: logs,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function getEmailLogById(id: number) {
|
||||
return prisma.emailLog.findUnique({ where: { id } });
|
||||
}
|
||||
|
||||
export async function getEmailLogStats() {
|
||||
const [total, success, failed, last24h] = await Promise.all([
|
||||
prisma.emailLog.count(),
|
||||
prisma.emailLog.count({ where: { success: true } }),
|
||||
prisma.emailLog.count({ where: { success: false } }),
|
||||
prisma.emailLog.count({
|
||||
where: { sentAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) } },
|
||||
}),
|
||||
]);
|
||||
|
||||
return { total, success, failed, last24h };
|
||||
}
|
||||
|
|
@ -73,6 +73,9 @@ export interface CreateProviderConfigData {
|
|||
imapEncryption?: MailEncryption;
|
||||
smtpEncryption?: MailEncryption;
|
||||
allowSelfSignedCerts?: boolean;
|
||||
// System-E-Mail
|
||||
systemEmailAddress?: string;
|
||||
systemEmailPassword?: string;
|
||||
isActive?: boolean;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
|
@ -86,9 +89,10 @@ export async function createProviderConfig(data: CreateProviderConfigData) {
|
|||
});
|
||||
}
|
||||
|
||||
// Passwort verschlüsseln falls vorhanden
|
||||
// Passwörter verschlüsseln falls vorhanden
|
||||
const { encrypt } = await import('../../utils/encryption.js');
|
||||
const passwordEncrypted = data.password ? encrypt(data.password) : null;
|
||||
const systemEmailPasswordEncrypted = data.systemEmailPassword ? encrypt(data.systemEmailPassword) : null;
|
||||
|
||||
return prisma.emailProviderConfig.create({
|
||||
data: {
|
||||
|
|
@ -103,6 +107,8 @@ export async function createProviderConfig(data: CreateProviderConfigData) {
|
|||
imapEncryption: data.imapEncryption ?? 'SSL',
|
||||
smtpEncryption: data.smtpEncryption ?? 'SSL',
|
||||
allowSelfSignedCerts: data.allowSelfSignedCerts ?? false,
|
||||
systemEmailAddress: data.systemEmailAddress || null,
|
||||
systemEmailPasswordEncrypted,
|
||||
isActive: data.isActive ?? true,
|
||||
isDefault: data.isDefault ?? false,
|
||||
},
|
||||
|
|
@ -134,20 +140,30 @@ export async function updateProviderConfig(
|
|||
if (data.imapEncryption !== undefined) updateData.imapEncryption = data.imapEncryption;
|
||||
if (data.smtpEncryption !== undefined) updateData.smtpEncryption = data.smtpEncryption;
|
||||
if (data.allowSelfSignedCerts !== undefined) updateData.allowSelfSignedCerts = data.allowSelfSignedCerts;
|
||||
if (data.systemEmailAddress !== undefined) updateData.systemEmailAddress = data.systemEmailAddress || null;
|
||||
if (data.isActive !== undefined) updateData.isActive = data.isActive;
|
||||
if (data.isDefault !== undefined) updateData.isDefault = data.isDefault;
|
||||
|
||||
const { encrypt } = await import('../../utils/encryption.js');
|
||||
|
||||
// Passwort-Logik:
|
||||
// - Wenn neues Passwort übergeben → verschlüsseln und speichern
|
||||
// - Wenn Benutzername gelöscht wird → Passwort auch löschen (gehören zusammen)
|
||||
if (data.password) {
|
||||
const { encrypt } = await import('../../utils/encryption.js');
|
||||
updateData.passwordEncrypted = encrypt(data.password);
|
||||
} else if (data.username !== undefined && !data.username) {
|
||||
// Benutzername wird gelöscht → Passwort auch löschen
|
||||
updateData.passwordEncrypted = null;
|
||||
}
|
||||
|
||||
// System-E-Mail-Passwort
|
||||
if (data.systemEmailPassword) {
|
||||
updateData.systemEmailPasswordEncrypted = encrypt(data.systemEmailPassword);
|
||||
} else if (data.systemEmailAddress !== undefined && !data.systemEmailAddress) {
|
||||
// System-E-Mail wird gelöscht → Passwort auch löschen
|
||||
updateData.systemEmailPasswordEncrypted = null;
|
||||
}
|
||||
|
||||
return prisma.emailProviderConfig.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
|
|
@ -564,3 +580,45 @@ export async function testProviderConnection(options?: {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SYSTEM EMAIL ====================
|
||||
|
||||
export interface SystemEmailCredentials {
|
||||
emailAddress: string;
|
||||
password: string;
|
||||
smtpServer: string;
|
||||
smtpPort: number;
|
||||
smtpEncryption: MailEncryption;
|
||||
allowSelfSignedCerts: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* System-E-Mail-Credentials vom aktiven Provider holen.
|
||||
* Wird für automatisierten Versand (DSGVO, Benachrichtigungen etc.) verwendet.
|
||||
*/
|
||||
export async function getSystemEmailCredentials(): Promise<SystemEmailCredentials | null> {
|
||||
const config = await getActiveProviderConfig();
|
||||
if (!config?.systemEmailAddress || !config?.systemEmailPasswordEncrypted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let password: string;
|
||||
try {
|
||||
password = decrypt(config.systemEmailPasswordEncrypted);
|
||||
} catch {
|
||||
console.error('System-E-Mail-Passwort konnte nicht entschlüsselt werden');
|
||||
return null;
|
||||
}
|
||||
|
||||
const settings = await getImapSmtpSettings();
|
||||
if (!settings) return null;
|
||||
|
||||
return {
|
||||
emailAddress: config.systemEmailAddress,
|
||||
password,
|
||||
smtpServer: settings.smtpServer,
|
||||
smtpPort: settings.smtpPort,
|
||||
smtpEncryption: settings.smtpEncryption,
|
||||
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,565 @@
|
|||
import { DeletionRequestStatus } from '@prisma/client';
|
||||
import prisma from '../lib/prisma.js';
|
||||
import { getAuditLogsByDataSubject } from './audit.service.js';
|
||||
import { getCustomerConsents, withdrawAllConsents } from './consent.service.js';
|
||||
import PDFDocument from 'pdfkit';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export interface CreateDeletionRequestData {
|
||||
customerId: number;
|
||||
requestSource: string;
|
||||
requestedBy: string;
|
||||
}
|
||||
|
||||
export interface ProcessDeletionRequestData {
|
||||
processedBy: string;
|
||||
action: 'complete' | 'partial' | 'reject';
|
||||
retentionReason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exportiert alle Daten eines Kunden (DSGVO Art. 15)
|
||||
*/
|
||||
export async function exportCustomerData(customerId: number) {
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
include: {
|
||||
addresses: true,
|
||||
bankCards: {
|
||||
select: {
|
||||
id: true,
|
||||
accountHolder: true,
|
||||
iban: true,
|
||||
bic: true,
|
||||
bankName: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
identityDocuments: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
documentNumber: true,
|
||||
issuingAuthority: true,
|
||||
issueDate: true,
|
||||
expiryDate: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
meters: {
|
||||
include: {
|
||||
readings: true,
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
include: {
|
||||
address: true,
|
||||
billingAddress: true,
|
||||
provider: true,
|
||||
tariff: true,
|
||||
energyDetails: {
|
||||
include: { invoices: true },
|
||||
},
|
||||
internetDetails: {
|
||||
include: { phoneNumbers: true },
|
||||
},
|
||||
mobileDetails: {
|
||||
include: { simCards: true },
|
||||
},
|
||||
tvDetails: true,
|
||||
carInsuranceDetails: true,
|
||||
historyEntries: true,
|
||||
tasks: {
|
||||
include: { subtasks: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
stressfreiEmails: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
platform: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
consents: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new Error('Kunde nicht gefunden');
|
||||
}
|
||||
|
||||
// Audit-Logs für diesen Kunden
|
||||
const accessLogs = await getAuditLogsByDataSubject(customerId);
|
||||
|
||||
// Sensible Felder entfernen
|
||||
const exportData = {
|
||||
exportDate: new Date().toISOString(),
|
||||
dataSubject: {
|
||||
id: customer.id,
|
||||
customerNumber: customer.customerNumber,
|
||||
name: `${customer.firstName} ${customer.lastName}`,
|
||||
},
|
||||
personalData: {
|
||||
salutation: customer.salutation,
|
||||
firstName: customer.firstName,
|
||||
lastName: customer.lastName,
|
||||
companyName: customer.companyName,
|
||||
type: customer.type,
|
||||
birthDate: customer.birthDate,
|
||||
birthPlace: customer.birthPlace,
|
||||
email: customer.email,
|
||||
phone: customer.phone,
|
||||
mobile: customer.mobile,
|
||||
taxNumber: customer.taxNumber,
|
||||
portalEnabled: customer.portalEnabled,
|
||||
portalEmail: customer.portalEmail,
|
||||
portalLastLogin: customer.portalLastLogin,
|
||||
createdAt: customer.createdAt,
|
||||
updatedAt: customer.updatedAt,
|
||||
},
|
||||
addresses: customer.addresses,
|
||||
bankCards: customer.bankCards,
|
||||
identityDocuments: customer.identityDocuments,
|
||||
meters: customer.meters,
|
||||
contracts: customer.contracts.map((c) => ({
|
||||
...c,
|
||||
// Sensible Daten entfernen
|
||||
portalPasswordEncrypted: undefined,
|
||||
})),
|
||||
emails: customer.stressfreiEmails,
|
||||
consents: customer.consents,
|
||||
accessHistory: accessLogs.map((log) => ({
|
||||
timestamp: log.createdAt,
|
||||
action: log.action,
|
||||
user: log.userEmail,
|
||||
resource: log.resourceType,
|
||||
ipAddress: log.ipAddress,
|
||||
})),
|
||||
};
|
||||
|
||||
return exportData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Löschanfrage
|
||||
*/
|
||||
export async function createDeletionRequest(data: CreateDeletionRequestData) {
|
||||
// Prüfen ob Kunde existiert
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: data.customerId },
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new Error('Kunde nicht gefunden');
|
||||
}
|
||||
|
||||
// Prüfen ob bereits eine offene Anfrage existiert
|
||||
const existingRequest = await prisma.dataDeletionRequest.findFirst({
|
||||
where: {
|
||||
customerId: data.customerId,
|
||||
status: { in: ['PENDING', 'IN_PROGRESS'] },
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRequest) {
|
||||
throw new Error('Es existiert bereits eine offene Löschanfrage für diesen Kunden');
|
||||
}
|
||||
|
||||
return prisma.dataDeletionRequest.create({
|
||||
data: {
|
||||
customerId: data.customerId,
|
||||
requestSource: data.requestSource,
|
||||
requestedBy: data.requestedBy,
|
||||
status: 'PENDING',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt alle Löschanfragen mit Paginierung
|
||||
*/
|
||||
export async function getDeletionRequests(params: {
|
||||
status?: DeletionRequestStatus;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) {
|
||||
const { status, page = 1, limit = 20 } = params;
|
||||
|
||||
const where = status ? { status } : {};
|
||||
|
||||
const [requests, total] = await Promise.all([
|
||||
prisma.dataDeletionRequest.findMany({
|
||||
where,
|
||||
orderBy: { requestedAt: 'desc' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.dataDeletionRequest.count({ where }),
|
||||
]);
|
||||
|
||||
// Kundendaten hinzufügen
|
||||
const customerIds = requests.map((r) => r.customerId);
|
||||
const customers = await prisma.customer.findMany({
|
||||
where: { id: { in: customerIds } },
|
||||
select: {
|
||||
id: true,
|
||||
customerNumber: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
});
|
||||
|
||||
const customerMap = new Map(customers.map((c) => [c.id, c]));
|
||||
|
||||
const requestsWithCustomer = requests.map((r) => ({
|
||||
...r,
|
||||
customer: customerMap.get(r.customerId) || null,
|
||||
}));
|
||||
|
||||
return {
|
||||
data: requestsWithCustomer,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt eine einzelne Löschanfrage
|
||||
*/
|
||||
export async function getDeletionRequest(id: number) {
|
||||
const request = await prisma.dataDeletionRequest.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!request) return null;
|
||||
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: request.customerId },
|
||||
select: {
|
||||
id: true,
|
||||
customerNumber: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { ...request, customer };
|
||||
}
|
||||
|
||||
/**
|
||||
* Bearbeitet eine Löschanfrage
|
||||
*/
|
||||
export async function processDeletionRequest(
|
||||
requestId: number,
|
||||
data: ProcessDeletionRequestData
|
||||
) {
|
||||
const request = await prisma.dataDeletionRequest.findUnique({
|
||||
where: { id: requestId },
|
||||
});
|
||||
|
||||
if (!request) {
|
||||
throw new Error('Löschanfrage nicht gefunden');
|
||||
}
|
||||
|
||||
if (request.status !== 'PENDING' && request.status !== 'IN_PROGRESS') {
|
||||
throw new Error('Diese Anfrage wurde bereits bearbeitet');
|
||||
}
|
||||
|
||||
// Status auf IN_PROGRESS setzen
|
||||
await prisma.dataDeletionRequest.update({
|
||||
where: { id: requestId },
|
||||
data: { status: 'IN_PROGRESS' },
|
||||
});
|
||||
|
||||
const customerId = request.customerId;
|
||||
const deletedData: Record<string, number> = {};
|
||||
const retainedData: Record<string, { count: number; reason: string }> = {};
|
||||
|
||||
try {
|
||||
if (data.action === 'reject') {
|
||||
// Anfrage ablehnen
|
||||
return prisma.dataDeletionRequest.update({
|
||||
where: { id: requestId },
|
||||
data: {
|
||||
status: 'REJECTED',
|
||||
processedAt: new Date(),
|
||||
processedBy: data.processedBy,
|
||||
retentionReason: data.retentionReason,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Einwilligungen widerrufen
|
||||
await withdrawAllConsents(customerId, data.processedBy);
|
||||
deletedData['consents'] = 1;
|
||||
|
||||
// Verträge prüfen - aktive Verträge müssen behalten werden
|
||||
const contracts = await prisma.contract.findMany({
|
||||
where: { customerId },
|
||||
});
|
||||
|
||||
const activeContracts = contracts.filter(
|
||||
(c) => c.status === 'ACTIVE' || c.status === 'PENDING'
|
||||
);
|
||||
|
||||
if (activeContracts.length > 0) {
|
||||
retainedData['contracts'] = {
|
||||
count: activeContracts.length,
|
||||
reason: 'Aktive Verträge müssen für die Vertragserfüllung aufbewahrt werden',
|
||||
};
|
||||
}
|
||||
|
||||
// Löschbare Daten anonymisieren (statt hart löschen)
|
||||
if (data.action === 'complete' && activeContracts.length === 0) {
|
||||
// Kunde vollständig anonymisieren
|
||||
await anonymizeCustomer(customerId);
|
||||
deletedData['customer'] = 1;
|
||||
deletedData['addresses'] = 1;
|
||||
deletedData['bankCards'] = 1;
|
||||
deletedData['identityDocuments'] = 1;
|
||||
} else {
|
||||
// Teilweise Löschung - nur optionale Daten
|
||||
const deletedAddresses = await prisma.address.deleteMany({
|
||||
where: { customerId, isDefault: false },
|
||||
});
|
||||
deletedData['addresses'] = deletedAddresses.count;
|
||||
|
||||
// Inaktive Bankkarten löschen
|
||||
const deletedBankCards = await prisma.bankCard.deleteMany({
|
||||
where: { customerId, isActive: false },
|
||||
});
|
||||
deletedData['bankCards'] = deletedBankCards.count;
|
||||
|
||||
// Inaktive Dokumente löschen
|
||||
const deletedDocs = await prisma.identityDocument.deleteMany({
|
||||
where: { customerId, isActive: false },
|
||||
});
|
||||
deletedData['identityDocuments'] = deletedDocs.count;
|
||||
}
|
||||
|
||||
// Löschnachweis generieren
|
||||
const proofPath = await generateDeletionProof(requestId, customerId, deletedData, retainedData);
|
||||
|
||||
// Anfrage abschließen
|
||||
const status = Object.keys(retainedData).length > 0 ? 'PARTIALLY_COMPLETED' : 'COMPLETED';
|
||||
|
||||
return prisma.dataDeletionRequest.update({
|
||||
where: { id: requestId },
|
||||
data: {
|
||||
status,
|
||||
processedAt: new Date(),
|
||||
processedBy: data.processedBy,
|
||||
deletedData: JSON.stringify(deletedData),
|
||||
retainedData: JSON.stringify(retainedData),
|
||||
retentionReason: data.retentionReason,
|
||||
proofDocument: proofPath,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// Bei Fehler Status zurücksetzen
|
||||
await prisma.dataDeletionRequest.update({
|
||||
where: { id: requestId },
|
||||
data: { status: 'PENDING' },
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonymisiert Kundendaten (DSGVO-konform)
|
||||
*/
|
||||
async function anonymizeCustomer(customerId: number) {
|
||||
const anonymized = `[GELÖSCHT-${Date.now()}]`;
|
||||
|
||||
await prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: {
|
||||
firstName: anonymized,
|
||||
lastName: anonymized,
|
||||
salutation: null,
|
||||
companyName: null,
|
||||
birthDate: null,
|
||||
birthPlace: null,
|
||||
email: null,
|
||||
phone: null,
|
||||
mobile: null,
|
||||
taxNumber: null,
|
||||
notes: null,
|
||||
portalEnabled: false,
|
||||
portalEmail: null,
|
||||
portalPasswordHash: null,
|
||||
portalPasswordEncrypted: null,
|
||||
},
|
||||
});
|
||||
|
||||
// Adressen anonymisieren
|
||||
await prisma.address.updateMany({
|
||||
where: { customerId },
|
||||
data: {
|
||||
street: anonymized,
|
||||
houseNumber: '',
|
||||
postalCode: '00000',
|
||||
city: anonymized,
|
||||
},
|
||||
});
|
||||
|
||||
// Bankkarten anonymisieren
|
||||
await prisma.bankCard.updateMany({
|
||||
where: { customerId },
|
||||
data: {
|
||||
accountHolder: anonymized,
|
||||
iban: 'XX00000000000000000000',
|
||||
bic: null,
|
||||
bankName: null,
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Ausweisdokumente anonymisieren
|
||||
await prisma.identityDocument.updateMany({
|
||||
where: { customerId },
|
||||
data: {
|
||||
documentNumber: anonymized,
|
||||
issuingAuthority: null,
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert ein Löschnachweis-PDF
|
||||
*/
|
||||
async function generateDeletionProof(
|
||||
requestId: number,
|
||||
customerId: number,
|
||||
deletedData: Record<string, number>,
|
||||
retainedData: Record<string, { count: number; reason: string }>
|
||||
): Promise<string> {
|
||||
const uploadsDir = path.join(process.cwd(), 'uploads', 'gdpr');
|
||||
|
||||
// Verzeichnis erstellen falls nicht vorhanden
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
const filename = `loeschnachweis_${requestId}_${Date.now()}.pdf`;
|
||||
const filepath = path.join(uploadsDir, filename);
|
||||
|
||||
const doc = new PDFDocument({ size: 'A4', margin: 50 });
|
||||
const writeStream = fs.createWriteStream(filepath);
|
||||
doc.pipe(writeStream);
|
||||
|
||||
// Titel
|
||||
doc.fontSize(18).text('Datenlöschungsnachweis', { align: 'center' });
|
||||
doc.moveDown();
|
||||
|
||||
// Metadaten
|
||||
doc.fontSize(12);
|
||||
doc.text(`Anfrage-ID: ${requestId}`);
|
||||
doc.text(`Kunden-ID: ${customerId}`);
|
||||
doc.text(`Datum: ${new Date().toLocaleDateString('de-DE')}`);
|
||||
doc.text(`Uhrzeit: ${new Date().toLocaleTimeString('de-DE')}`);
|
||||
doc.moveDown();
|
||||
|
||||
// Gelöschte Daten
|
||||
doc.fontSize(14).text('Gelöschte Daten:', { underline: true });
|
||||
doc.fontSize(12);
|
||||
for (const [category, count] of Object.entries(deletedData)) {
|
||||
doc.text(`• ${category}: ${count} Einträge`);
|
||||
}
|
||||
doc.moveDown();
|
||||
|
||||
// Aufbewahrte Daten
|
||||
if (Object.keys(retainedData).length > 0) {
|
||||
doc.fontSize(14).text('Aufbewahrte Daten:', { underline: true });
|
||||
doc.fontSize(12);
|
||||
for (const [category, info] of Object.entries(retainedData)) {
|
||||
doc.text(`• ${category}: ${info.count} Einträge`);
|
||||
doc.fontSize(10).text(` Grund: ${info.reason}`, { indent: 20 });
|
||||
doc.fontSize(12);
|
||||
}
|
||||
doc.moveDown();
|
||||
}
|
||||
|
||||
// Rechtlicher Hinweis
|
||||
doc.moveDown();
|
||||
doc.fontSize(10).text(
|
||||
'Dieses Dokument bestätigt die Durchführung der Datenlöschung gemäß Art. 17 DSGVO. ' +
|
||||
'Daten, die aus gesetzlichen Gründen aufbewahrt werden müssen, wurden nicht gelöscht.',
|
||||
{ align: 'justify' }
|
||||
);
|
||||
|
||||
doc.end();
|
||||
|
||||
// Warten bis Datei geschrieben wurde
|
||||
await new Promise<void>((resolve) => writeStream.on('finish', resolve));
|
||||
|
||||
return `gdpr/${filename}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard-Statistiken für DSGVO
|
||||
*/
|
||||
export async function getGDPRDashboardStats() {
|
||||
const [
|
||||
pendingDeletions,
|
||||
completedDeletions,
|
||||
recentExports,
|
||||
consentStats,
|
||||
] = await Promise.all([
|
||||
// Offene Löschanfragen
|
||||
prisma.dataDeletionRequest.count({
|
||||
where: { status: { in: ['PENDING', 'IN_PROGRESS'] } },
|
||||
}),
|
||||
// Abgeschlossene Löschungen (letzte 30 Tage)
|
||||
prisma.dataDeletionRequest.count({
|
||||
where: {
|
||||
status: { in: ['COMPLETED', 'PARTIALLY_COMPLETED'] },
|
||||
processedAt: { gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
|
||||
},
|
||||
}),
|
||||
// Letzte Datenexporte (aus Audit-Log)
|
||||
prisma.auditLog.count({
|
||||
where: {
|
||||
action: 'EXPORT',
|
||||
resourceType: 'GDPR',
|
||||
createdAt: { gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
|
||||
},
|
||||
}),
|
||||
// Consent-Statistik
|
||||
prisma.customerConsent.groupBy({
|
||||
by: ['status'],
|
||||
_count: { id: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
const consentByStatus = consentStats.reduce(
|
||||
(acc, s) => {
|
||||
acc[s.status.toLowerCase()] = s._count.id;
|
||||
return acc;
|
||||
},
|
||||
{ granted: 0, withdrawn: 0, pending: 0 } as Record<string, number>
|
||||
);
|
||||
|
||||
return {
|
||||
deletionRequests: {
|
||||
pending: pendingDeletions,
|
||||
completedLast30Days: completedDeletions,
|
||||
},
|
||||
dataExports: {
|
||||
last30Days: recentExports,
|
||||
},
|
||||
consents: consentByStatus,
|
||||
};
|
||||
}
|
||||
|
|
@ -42,11 +42,19 @@ export interface SendEmailResult {
|
|||
error?: string;
|
||||
}
|
||||
|
||||
// Optionaler Logging-Kontext
|
||||
export interface EmailLogContext {
|
||||
context: string; // z.B. "consent-link", "authorization-request", "customer-email"
|
||||
customerId?: number;
|
||||
triggeredBy?: string; // User-Email
|
||||
}
|
||||
|
||||
// E-Mail senden
|
||||
export async function sendEmail(
|
||||
credentials: SmtpCredentials,
|
||||
fromAddress: string,
|
||||
params: SendEmailParams
|
||||
params: SendEmailParams,
|
||||
logContext?: EmailLogContext
|
||||
): Promise<SendEmailResult> {
|
||||
// Verschlüsselungs-Einstellungen basierend auf Modus
|
||||
const encryption = credentials.encryption ?? 'SSL';
|
||||
|
|
@ -155,6 +163,27 @@ export async function sendEmail(
|
|||
// Nicht kritisch - E-Mail wurde trotzdem gesendet
|
||||
}
|
||||
|
||||
// E-Mail-Log erstellen (async, nicht blockierend)
|
||||
if (logContext) {
|
||||
import('./emailLog.service.js').then(({ createEmailLog }) => {
|
||||
createEmailLog({
|
||||
fromAddress,
|
||||
toAddress: Array.isArray(params.to) ? params.to.join(', ') : params.to,
|
||||
subject: params.subject,
|
||||
context: logContext.context,
|
||||
customerId: logContext.customerId,
|
||||
triggeredBy: logContext.triggeredBy,
|
||||
smtpServer: credentials.host,
|
||||
smtpPort: credentials.port,
|
||||
smtpEncryption: credentials.encryption ?? 'SSL',
|
||||
smtpUser: credentials.user,
|
||||
success: true,
|
||||
messageId: result.messageId,
|
||||
smtpResponse: result.response,
|
||||
}).catch((err) => console.error('EmailLog write error:', err));
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
messageId: result.messageId,
|
||||
|
|
@ -203,6 +232,26 @@ export async function sendEmail(
|
|||
}
|
||||
}
|
||||
|
||||
// E-Mail-Log erstellen (Fehler)
|
||||
if (logContext) {
|
||||
import('./emailLog.service.js').then(({ createEmailLog }) => {
|
||||
createEmailLog({
|
||||
fromAddress,
|
||||
toAddress: Array.isArray(params.to) ? params.to.join(', ') : params.to,
|
||||
subject: params.subject,
|
||||
context: logContext.context,
|
||||
customerId: logContext.customerId,
|
||||
triggeredBy: logContext.triggeredBy,
|
||||
smtpServer: credentials.host,
|
||||
smtpPort: credentials.port,
|
||||
smtpEncryption: credentials.encryption ?? 'SSL',
|
||||
smtpUser: credentials.user,
|
||||
success: false,
|
||||
errorMessage,
|
||||
}).catch((err) => console.error('EmailLog write error:', err));
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ export async function getAllUsers(filters: UserFilters) {
|
|||
lastName: true,
|
||||
isActive: true,
|
||||
customerId: true,
|
||||
whatsappNumber: true,
|
||||
telegramUsername: true,
|
||||
signalNumber: true,
|
||||
createdAt: true,
|
||||
roles: {
|
||||
include: {
|
||||
|
|
@ -62,21 +65,25 @@ export async function getAllUsers(filters: UserFilters) {
|
|||
prisma.user.count({ where }),
|
||||
]);
|
||||
|
||||
// Get Developer role ID
|
||||
const developerRole = await prisma.role.findFirst({
|
||||
where: { name: 'Developer' },
|
||||
});
|
||||
// Get hidden role IDs
|
||||
const [developerRole, gdprRole] = await Promise.all([
|
||||
prisma.role.findFirst({ where: { name: 'Developer' } }),
|
||||
prisma.role.findFirst({ where: { name: 'DSGVO' } }),
|
||||
]);
|
||||
|
||||
return {
|
||||
users: users.map((u) => {
|
||||
// Check if user has developer role assigned
|
||||
const hasDeveloperAccess = developerRole
|
||||
? u.roles.some((ur) => ur.roleId === developerRole.id)
|
||||
: false;
|
||||
const hasGdprAccess = gdprRole
|
||||
? u.roles.some((ur) => ur.roleId === gdprRole.id)
|
||||
: false;
|
||||
return {
|
||||
...u,
|
||||
roles: u.roles.map((r) => r.role),
|
||||
hasDeveloperAccess,
|
||||
hasGdprAccess,
|
||||
};
|
||||
}),
|
||||
pagination: buildPaginationResponse(page, limit, total),
|
||||
|
|
@ -93,6 +100,9 @@ export async function getUserById(id: number) {
|
|||
lastName: true,
|
||||
isActive: true,
|
||||
customerId: true,
|
||||
whatsappNumber: true,
|
||||
telegramUsername: true,
|
||||
signalNumber: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
roles: {
|
||||
|
|
@ -135,6 +145,10 @@ export async function createUser(data: {
|
|||
roleIds: number[];
|
||||
customerId?: number;
|
||||
hasDeveloperAccess?: boolean;
|
||||
hasGdprAccess?: boolean;
|
||||
whatsappNumber?: string;
|
||||
telegramUsername?: string;
|
||||
signalNumber?: string;
|
||||
}) {
|
||||
const hashedPassword = await bcrypt.hash(data.password, 10);
|
||||
|
||||
|
|
@ -145,6 +159,9 @@ export async function createUser(data: {
|
|||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
customerId: data.customerId,
|
||||
whatsappNumber: data.whatsappNumber || null,
|
||||
telegramUsername: data.telegramUsername || null,
|
||||
signalNumber: data.signalNumber || null,
|
||||
roles: {
|
||||
create: data.roleIds.map((roleId) => ({ roleId })),
|
||||
},
|
||||
|
|
@ -167,6 +184,11 @@ export async function createUser(data: {
|
|||
await setUserDeveloperAccess(user.id, true);
|
||||
}
|
||||
|
||||
// DSGVO-Zugriff setzen falls aktiviert
|
||||
if (data.hasGdprAccess) {
|
||||
await setUserGdprAccess(user.id, true);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
|
|
@ -181,9 +203,13 @@ export async function updateUser(
|
|||
roleIds?: number[];
|
||||
customerId?: number;
|
||||
hasDeveloperAccess?: boolean;
|
||||
hasGdprAccess?: boolean;
|
||||
whatsappNumber?: string;
|
||||
telegramUsername?: string;
|
||||
signalNumber?: string;
|
||||
}
|
||||
) {
|
||||
const { roleIds, password, hasDeveloperAccess, ...userData } = data;
|
||||
const { roleIds, password, hasDeveloperAccess, hasGdprAccess, ...userData } = data;
|
||||
|
||||
// Check if this would remove the last admin
|
||||
const isBeingDeactivated = userData.isActive === false;
|
||||
|
|
@ -311,18 +337,20 @@ export async function updateUser(
|
|||
}
|
||||
|
||||
// Handle developer access
|
||||
console.log('updateUser - hasDeveloperAccess:', hasDeveloperAccess);
|
||||
if (hasDeveloperAccess !== undefined) {
|
||||
await setUserDeveloperAccess(id, hasDeveloperAccess);
|
||||
}
|
||||
|
||||
// Handle GDPR access
|
||||
if (hasGdprAccess !== undefined) {
|
||||
await setUserGdprAccess(id, hasGdprAccess);
|
||||
}
|
||||
|
||||
return getUserById(id);
|
||||
}
|
||||
|
||||
// Helper to set developer access for a user
|
||||
async function setUserDeveloperAccess(userId: number, enabled: boolean) {
|
||||
console.log('setUserDeveloperAccess called - userId:', userId, 'enabled:', enabled);
|
||||
|
||||
// Get or create developer:access permission
|
||||
let developerPerm = await prisma.permission.findFirst({
|
||||
where: { resource: 'developer', action: 'access' },
|
||||
|
|
@ -356,11 +384,7 @@ async function setUserDeveloperAccess(userId: number, enabled: boolean) {
|
|||
where: { userId, roleId: developerRole.id },
|
||||
});
|
||||
|
||||
console.log('setUserDeveloperAccess - developerRole.id:', developerRole.id, 'hasRole:', hasRole);
|
||||
|
||||
if (enabled && !hasRole) {
|
||||
// Add Developer role
|
||||
console.log('Adding Developer role');
|
||||
await prisma.userRole.create({
|
||||
data: { userId, roleId: developerRole.id },
|
||||
});
|
||||
|
|
@ -370,8 +394,6 @@ async function setUserDeveloperAccess(userId: number, enabled: boolean) {
|
|||
data: { tokenInvalidatedAt: new Date() },
|
||||
});
|
||||
} else if (!enabled && hasRole) {
|
||||
// Remove Developer role
|
||||
console.log('Removing Developer role');
|
||||
await prisma.userRole.delete({
|
||||
where: { userId_roleId: { userId, roleId: developerRole.id } },
|
||||
});
|
||||
|
|
@ -380,8 +402,56 @@ async function setUserDeveloperAccess(userId: number, enabled: boolean) {
|
|||
where: { id: userId },
|
||||
data: { tokenInvalidatedAt: new Date() },
|
||||
});
|
||||
} else {
|
||||
console.log('No action needed - enabled:', enabled, 'hasRole:', !!hasRole);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to set GDPR access for a user
|
||||
async function setUserGdprAccess(userId: number, enabled: boolean) {
|
||||
// Get or create DSGVO role
|
||||
let gdprRole = await prisma.role.findFirst({
|
||||
where: { name: 'DSGVO' },
|
||||
});
|
||||
|
||||
if (!gdprRole) {
|
||||
// Create DSGVO role with all audit:* and gdpr:* permissions
|
||||
const gdprPermissions = await prisma.permission.findMany({
|
||||
where: {
|
||||
OR: [{ resource: 'audit' }, { resource: 'gdpr' }],
|
||||
},
|
||||
});
|
||||
|
||||
gdprRole = await prisma.role.create({
|
||||
data: {
|
||||
name: 'DSGVO',
|
||||
description: 'DSGVO-Zugriff: Audit-Logs und Datenschutz-Verwaltung',
|
||||
permissions: {
|
||||
create: gdprPermissions.map((p) => ({ permissionId: p.id })),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user already has DSGVO role
|
||||
const hasRole = await prisma.userRole.findFirst({
|
||||
where: { userId, roleId: gdprRole.id },
|
||||
});
|
||||
|
||||
if (enabled && !hasRole) {
|
||||
await prisma.userRole.create({
|
||||
data: { userId, roleId: gdprRole.id },
|
||||
});
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { tokenInvalidatedAt: new Date() },
|
||||
});
|
||||
} else if (!enabled && hasRole) {
|
||||
await prisma.userRole.delete({
|
||||
where: { userId_roleId: { userId, roleId: gdprRole.id } },
|
||||
});
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { tokenInvalidatedAt: new Date() },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -5,8 +5,8 @@
|
|||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OpenCRM</title>
|
||||
<script type="module" crossorigin src="/assets/index-AsLwOxex.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-b8RXSgxB.css">
|
||||
<script type="module" crossorigin src="/assets/index-BLUL0czD.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-dzAFnrZs.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
../markdown-it/bin/markdown-it.mjs
|
||||
|
|
@ -295,6 +295,31 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz",
|
||||
"integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz",
|
||||
"integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.4",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
|
|
@ -375,6 +400,11 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@remirror/core-constants": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
|
||||
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg=="
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
||||
|
|
@ -439,6 +469,412 @@
|
|||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/core": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.19.0.tgz",
|
||||
"integrity": "sha512-bpqELwPW+DG8gWiD8iiFtSl4vIBooG5uVJod92Qxn3rA9nFatyXRr4kNbMJmOZ66ezUvmCjXVe/5/G4i5cyzKA==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/pm": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-blockquote": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.19.0.tgz",
|
||||
"integrity": "sha512-y3UfqY9KD5XwWz3ndiiJ089Ij2QKeiXy/g1/tlAN/F1AaWsnkHEHMLxCP1BIqmMpwsX7rZjMLN7G5Lp7c9682A==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bold": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.19.0.tgz",
|
||||
"integrity": "sha512-UZgb1d0XK4J/JRIZ7jW+s4S6KjuEDT2z1PPM6ugcgofgJkWQvRZelCPbmtSFd3kwsD+zr9UPVgTh9YIuGQ8t+Q==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bubble-menu": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.19.0.tgz",
|
||||
"integrity": "sha512-klNVIYGCdznhFkrRokzGd6cwzoi8J7E5KbuOfZBwFwhMKZhlz/gJfKmYg9TJopeUhrr2Z9yHgWTk8dh/YIJCdQ==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0",
|
||||
"@tiptap/pm": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bullet-list": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.19.0.tgz",
|
||||
"integrity": "sha512-F9uNnqd0xkJbMmRxVI5RuVxwB9JaCH/xtRqOUNQZnRBt7IdAElCY+Dvb4hMCtiNv+enGM/RFGJuFHR9TxmI7rw==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.19.0.tgz",
|
||||
"integrity": "sha512-2kqqQIXBXj2Or+4qeY3WoE7msK+XaHKL6EKOcKlOP2BW8eYqNTPzNSL+PfBDQ3snA7ljZQkTs/j4GYDj90vR1A==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code-block": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.19.0.tgz",
|
||||
"integrity": "sha512-b/2qR+tMn8MQb+eaFYgVk4qXnLNkkRYmwELQ8LEtEDQPxa5Vl7J3eu8+4OyoIFhZrNDZvvoEp80kHMCP8sI6rg==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0",
|
||||
"@tiptap/pm": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-document": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.19.0.tgz",
|
||||
"integrity": "sha512-AOf0kHKSFO0ymjVgYSYDncRXTITdTcrj1tqxVazrmO60KNl1Rc2dAggDvIVTEBy5NvceF0scc7q3sE/5ZtVV7A==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-dropcursor": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.19.0.tgz",
|
||||
"integrity": "sha512-sf3dEZXiLvsGqVK2maUIzXY6qtYYCvBumag7+VPTMGQ0D4hiZ1X/4ukt4+6VXDg5R2WP1CoIt/QvUetUjWNhbQ==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-floating-menu": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.19.0.tgz",
|
||||
"integrity": "sha512-JaoEkVRkt+Slq3tySlIsxnMnCjS0L5n1CA1hctjLy0iah8edetj3XD5mVv5iKqDzE+LIjF4nwLRRVKJPc8hFBg==",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@floating-ui/dom": "^1.0.0",
|
||||
"@tiptap/core": "^3.19.0",
|
||||
"@tiptap/pm": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-gapcursor": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.19.0.tgz",
|
||||
"integrity": "sha512-w7DACS4oSZaDWjz7gropZHPc9oXqC9yERZTcjWxyORuuIh1JFf0TRYspleK+OK28plK/IftojD/yUDn1MTRhvA==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-hard-break": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.19.0.tgz",
|
||||
"integrity": "sha512-lAmQraYhPS5hafvCl74xDB5+bLuNwBKIEsVoim35I0sDJj5nTrfhaZgMJ91VamMvT+6FF5f1dvBlxBxAWa8jew==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-heading": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.19.0.tgz",
|
||||
"integrity": "sha512-uLpLlfyp086WYNOc0ekm1gIZNlEDfmzOhKzB0Hbyi6jDagTS+p9mxUNYeYOn9jPUxpFov43+Wm/4E24oY6B+TQ==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.19.0.tgz",
|
||||
"integrity": "sha512-iqUHmgMGhMgYGwG6L/4JdelVQ5Mstb4qHcgTGd/4dkcUOepILvhdxajPle7OEdf9sRgjQO6uoAU5BVZVC26+ng==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0",
|
||||
"@tiptap/pm": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-italic": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.19.0.tgz",
|
||||
"integrity": "sha512-6GffxOnS/tWyCbDkirWNZITiXRta9wrCmrfa4rh+v32wfaOL1RRQNyqo9qN6Wjyl1R42Js+yXTzTTzZsOaLMYA==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-link": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.19.0.tgz",
|
||||
"integrity": "sha512-HEGDJnnCPfr7KWu7Dsq+eRRe/mBCsv6DuI+7fhOCLDJjjKzNgrX2abbo/zG3D/4lCVFaVb+qawgJubgqXR/Smw==",
|
||||
"dependencies": {
|
||||
"linkifyjs": "^4.3.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0",
|
||||
"@tiptap/pm": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.19.0.tgz",
|
||||
"integrity": "sha512-N6nKbFB2VwMsPlCw67RlAtYSK48TAsAUgjnD+vd3ieSlIufdQnLXDFUP6hFKx9mwoUVUgZGz02RA6bkxOdYyTw==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0",
|
||||
"@tiptap/pm": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list-item": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.19.0.tgz",
|
||||
"integrity": "sha512-VsSKuJz4/Tb6ZmFkXqWpDYkRzmaLTyE6dNSEpNmUpmZ32sMqo58mt11/huADNwfBFB0Ve7siH/VnFNIJYY3xvg==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list-keymap": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.19.0.tgz",
|
||||
"integrity": "sha512-bxgmAgA3RzBGA0GyTwS2CC1c+QjkJJq9hC+S6PSOWELGRiTbwDN3MANksFXLjntkTa0N5fOnL27vBHtMStURqw==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-ordered-list": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.19.0.tgz",
|
||||
"integrity": "sha512-cxGsINquwHYE1kmhAcLNLHAofmoDEG6jbesR5ybl7tU5JwtKVO7S/xZatll2DU1dsDAXWPWEeeMl4e/9svYjCg==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-paragraph": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.19.0.tgz",
|
||||
"integrity": "sha512-xWa6gj82l5+AzdYyrSk9P4ynySaDzg/SlR1FarXE5yPXibYzpS95IWaVR0m2Qaz7Rrk+IiYOTGxGRxcHLOelNg==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-strike": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.19.0.tgz",
|
||||
"integrity": "sha512-xYpabHsv7PccLUBQaP8AYiFCnYbx6P93RHPd0lgNwhdOjYFd931Zy38RyoxPHAgbYVmhf1iyx7lpuLtBnhS5dA==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.19.0.tgz",
|
||||
"integrity": "sha512-K95+SnbZy0h6hNFtfy23n8t/nOcTFEf69In9TSFVVmwn/Nwlke+IfiESAkqbt1/7sKJeegRXYO7WzFEmFl9Q/g==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-underline": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.19.0.tgz",
|
||||
"integrity": "sha512-800MGEWfG49j10wQzAFiW/ele1HT04MamcL8iyuPNu7ZbjbGN2yknvdrJlRy7hZlzIrVkZMr/1tz62KN33VHIw==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extensions": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.19.0.tgz",
|
||||
"integrity": "sha512-ZmGUhLbMWaGqnJh2Bry+6V4M6gMpUDYo4D1xNux5Gng/E/eYtc+PMxMZ/6F7tNTAuujLBOQKj6D+4SsSm457jw==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0",
|
||||
"@tiptap/pm": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/pm": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.19.0.tgz",
|
||||
"integrity": "sha512-789zcnM4a8OWzvbD2DL31d0wbSm9BVeO/R7PLQwLIGysDI3qzrcclyZ8yhqOEVuvPitRRwYLq+mY14jz7kY4cw==",
|
||||
"dependencies": {
|
||||
"prosemirror-changeset": "^2.3.0",
|
||||
"prosemirror-collab": "^1.3.1",
|
||||
"prosemirror-commands": "^1.6.2",
|
||||
"prosemirror-dropcursor": "^1.8.1",
|
||||
"prosemirror-gapcursor": "^1.3.2",
|
||||
"prosemirror-history": "^1.4.1",
|
||||
"prosemirror-inputrules": "^1.4.0",
|
||||
"prosemirror-keymap": "^1.2.2",
|
||||
"prosemirror-markdown": "^1.13.1",
|
||||
"prosemirror-menu": "^1.2.4",
|
||||
"prosemirror-model": "^1.24.1",
|
||||
"prosemirror-schema-basic": "^1.2.3",
|
||||
"prosemirror-schema-list": "^1.5.0",
|
||||
"prosemirror-state": "^1.4.3",
|
||||
"prosemirror-tables": "^1.6.4",
|
||||
"prosemirror-trailing-node": "^3.0.0",
|
||||
"prosemirror-transform": "^1.10.2",
|
||||
"prosemirror-view": "^1.38.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/react": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.19.0.tgz",
|
||||
"integrity": "sha512-GQQMUUXMpNd8tRjc1jDK3tDRXFugJO7C928EqmeBcBzTKDrFIJ3QUoZKEPxUNb6HWhZ2WL7q00fiMzsv4DNSmg==",
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"fast-equals": "^5.3.3",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tiptap/extension-bubble-menu": "^3.19.0",
|
||||
"@tiptap/extension-floating-menu": "^3.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.19.0",
|
||||
"@tiptap/pm": "^3.19.0",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/starter-kit": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.19.0.tgz",
|
||||
"integrity": "sha512-dTCkHEz+Y8ADxX7h+xvl6caAj+3nII/wMB1rTQchSuNKqJTOrzyUsCWm094+IoZmLT738wANE0fRIgziNHs/ug==",
|
||||
"dependencies": {
|
||||
"@tiptap/core": "^3.19.0",
|
||||
"@tiptap/extension-blockquote": "^3.19.0",
|
||||
"@tiptap/extension-bold": "^3.19.0",
|
||||
"@tiptap/extension-bullet-list": "^3.19.0",
|
||||
"@tiptap/extension-code": "^3.19.0",
|
||||
"@tiptap/extension-code-block": "^3.19.0",
|
||||
"@tiptap/extension-document": "^3.19.0",
|
||||
"@tiptap/extension-dropcursor": "^3.19.0",
|
||||
"@tiptap/extension-gapcursor": "^3.19.0",
|
||||
"@tiptap/extension-hard-break": "^3.19.0",
|
||||
"@tiptap/extension-heading": "^3.19.0",
|
||||
"@tiptap/extension-horizontal-rule": "^3.19.0",
|
||||
"@tiptap/extension-italic": "^3.19.0",
|
||||
"@tiptap/extension-link": "^3.19.0",
|
||||
"@tiptap/extension-list": "^3.19.0",
|
||||
"@tiptap/extension-list-item": "^3.19.0",
|
||||
"@tiptap/extension-list-keymap": "^3.19.0",
|
||||
"@tiptap/extension-ordered-list": "^3.19.0",
|
||||
"@tiptap/extension-paragraph": "^3.19.0",
|
||||
"@tiptap/extension-strike": "^3.19.0",
|
||||
"@tiptap/extension-text": "^3.19.0",
|
||||
"@tiptap/extension-underline": "^3.19.0",
|
||||
"@tiptap/extensions": "^3.19.0",
|
||||
"@tiptap/pm": "^3.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
|
|
@ -486,17 +922,34 @@
|
|||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "^5",
|
||||
"@types/mdurl": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.27",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
|
|
@ -506,11 +959,15 @@
|
|||
"version": "18.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||
|
|
@ -556,6 +1013,11 @@
|
|||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
|
|
@ -776,6 +1238,11 @@
|
|||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
|
|
@ -849,6 +1316,17 @@
|
|||
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
|
|
@ -937,6 +1415,25 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
|
||||
"integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
|
|
@ -1265,6 +1762,19 @@
|
|||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||
"dependencies": {
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/linkifyjs": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
|
||||
"integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
|
|
@ -1293,6 +1803,22 @@
|
|||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "14.1.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
|
||||
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.4.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"mdurl": "^2.0.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
"uc.micro": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
|
@ -1301,6 +1827,11 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
|
|
@ -1410,6 +1941,11 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/orderedmap": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
|
|
@ -1608,11 +2144,196 @@
|
|||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/prosemirror-changeset": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
|
||||
"integrity": "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==",
|
||||
"dependencies": {
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-collab": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
|
||||
"integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-commands": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
|
||||
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-dropcursor": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
|
||||
"integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0",
|
||||
"prosemirror-view": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-gapcursor": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz",
|
||||
"integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==",
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.0.0",
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-view": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-history": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
||||
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.2.2",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"prosemirror-view": "^1.31.0",
|
||||
"rope-sequence": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-inputrules": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
|
||||
"integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-keymap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||||
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"w3c-keyname": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-markdown": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz",
|
||||
"integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==",
|
||||
"dependencies": {
|
||||
"@types/markdown-it": "^14.0.0",
|
||||
"markdown-it": "^14.0.0",
|
||||
"prosemirror-model": "^1.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-menu": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz",
|
||||
"integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==",
|
||||
"dependencies": {
|
||||
"crelt": "^1.0.0",
|
||||
"prosemirror-commands": "^1.0.0",
|
||||
"prosemirror-history": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-model": {
|
||||
"version": "1.25.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||
"dependencies": {
|
||||
"orderedmap": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-schema-basic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
|
||||
"integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-schema-list": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
||||
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-state": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"prosemirror-view": "^1.27.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-tables": {
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
|
||||
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.2.3",
|
||||
"prosemirror-model": "^1.25.4",
|
||||
"prosemirror-state": "^1.4.4",
|
||||
"prosemirror-transform": "^1.10.5",
|
||||
"prosemirror-view": "^1.41.4"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-trailing-node": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
|
||||
"integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
|
||||
"dependencies": {
|
||||
"@remirror/core-constants": "3.0.0",
|
||||
"escape-string-regexp": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prosemirror-model": "^1.22.1",
|
||||
"prosemirror-state": "^1.4.2",
|
||||
"prosemirror-view": "^1.33.8"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-transform": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz",
|
||||
"integrity": "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-view": {
|
||||
"version": "1.41.6",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.6.tgz",
|
||||
"integrity": "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.20.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/punycode.js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
|
|
@ -1821,6 +2542,11 @@
|
|||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rope-sequence": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
|
|
@ -2038,6 +2764,11 @@
|
|||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
|
|
@ -2068,6 +2799,14 @@
|
|||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
|
@ -2133,6 +2872,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import {
|
||||
Link,
|
||||
index_default,
|
||||
isAllowedUri,
|
||||
pasteRegex
|
||||
} from "./chunk-MX7X7RGK.js";
|
||||
import "./chunk-YRIELJS7.js";
|
||||
import "./chunk-4MBMRILA.js";
|
||||
export {
|
||||
Link,
|
||||
index_default as default,
|
||||
isAllowedUri,
|
||||
pasteRegex
|
||||
};
|
||||
//# sourceMappingURL=@tiptap_extension-link.js.map
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -1,83 +1,107 @@
|
|||
{
|
||||
"hash": "b8db82d9",
|
||||
"hash": "3e94270a",
|
||||
"configHash": "c7be2068",
|
||||
"lockfileHash": "ee9bf28c",
|
||||
"browserHash": "deb47249",
|
||||
"lockfileHash": "c53fe6cb",
|
||||
"browserHash": "ee679691",
|
||||
"optimized": {
|
||||
"react": {
|
||||
"src": "../../react/index.js",
|
||||
"file": "react.js",
|
||||
"fileHash": "304f33a9",
|
||||
"fileHash": "c6967d06",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-dom": {
|
||||
"src": "../../react-dom/index.js",
|
||||
"file": "react-dom.js",
|
||||
"fileHash": "96d32bc1",
|
||||
"fileHash": "86783379",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-dev-runtime": {
|
||||
"src": "../../react/jsx-dev-runtime.js",
|
||||
"file": "react_jsx-dev-runtime.js",
|
||||
"fileHash": "6424b7ea",
|
||||
"fileHash": "d4bbc325",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-runtime": {
|
||||
"src": "../../react/jsx-runtime.js",
|
||||
"file": "react_jsx-runtime.js",
|
||||
"fileHash": "af95a7a1",
|
||||
"fileHash": "c4c48fd2",
|
||||
"needsInterop": true
|
||||
},
|
||||
"@tanstack/react-query": {
|
||||
"src": "../../@tanstack/react-query/build/modern/index.js",
|
||||
"file": "@tanstack_react-query.js",
|
||||
"fileHash": "c9459c14",
|
||||
"fileHash": "0ed0cd7e",
|
||||
"needsInterop": false
|
||||
},
|
||||
"axios": {
|
||||
"src": "../../axios/index.js",
|
||||
"file": "axios.js",
|
||||
"fileHash": "032e1913",
|
||||
"fileHash": "b14adf3a",
|
||||
"needsInterop": false
|
||||
},
|
||||
"lucide-react": {
|
||||
"src": "../../lucide-react/dist/esm/lucide-react.js",
|
||||
"file": "lucide-react.js",
|
||||
"fileHash": "341675db",
|
||||
"fileHash": "b370d795",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-dom/client": {
|
||||
"src": "../../react-dom/client.js",
|
||||
"file": "react-dom_client.js",
|
||||
"fileHash": "6520a32f",
|
||||
"fileHash": "17cccb93",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-hook-form": {
|
||||
"src": "../../react-hook-form/dist/index.esm.mjs",
|
||||
"file": "react-hook-form.js",
|
||||
"fileHash": "29e58164",
|
||||
"fileHash": "19a01ac0",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-router-dom": {
|
||||
"src": "../../react-router-dom/dist/index.js",
|
||||
"file": "react-router-dom.js",
|
||||
"fileHash": "4b5d4fdc",
|
||||
"fileHash": "427019e1",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-hot-toast": {
|
||||
"src": "../../react-hot-toast/dist/index.mjs",
|
||||
"file": "react-hot-toast.js",
|
||||
"fileHash": "7423c32d",
|
||||
"fileHash": "718bd15f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@tiptap/react": {
|
||||
"src": "../../@tiptap/react/dist/index.js",
|
||||
"file": "@tiptap_react.js",
|
||||
"fileHash": "c3b8784a",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@tiptap/starter-kit": {
|
||||
"src": "../../@tiptap/starter-kit/dist/index.js",
|
||||
"file": "@tiptap_starter-kit.js",
|
||||
"fileHash": "99ec846a",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@tiptap/extension-link": {
|
||||
"src": "../../@tiptap/extension-link/dist/index.js",
|
||||
"file": "@tiptap_extension-link.js",
|
||||
"fileHash": "884cdfc7",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"chunk-S77I6LSE": {
|
||||
"file": "chunk-S77I6LSE.js"
|
||||
"chunk-MX7X7RGK": {
|
||||
"file": "chunk-MX7X7RGK.js"
|
||||
},
|
||||
"chunk-YRIELJS7": {
|
||||
"file": "chunk-YRIELJS7.js"
|
||||
},
|
||||
"chunk-WERSD76P": {
|
||||
"file": "chunk-WERSD76P.js"
|
||||
},
|
||||
"chunk-S77I6LSE": {
|
||||
"file": "chunk-S77I6LSE.js"
|
||||
},
|
||||
"chunk-3TFVT2CW": {
|
||||
"file": "chunk-3TFVT2CW.js"
|
||||
},
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue