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:
duffyduck 2026-03-21 11:59:53 +01:00
parent 89cf92eaf5
commit f2876f877e
1491 changed files with 265550 additions and 1292 deletions

224
README.md
View File

@ -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

View File

@ -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"}

View File

@ -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

View File

@ -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>;

View File

@ -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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"}

View File

@ -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

14
backend/dist/index.js vendored
View File

@ -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() });

View File

@ -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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"}

View File

@ -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"}

View File

@ -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;
}

View File

@ -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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"}

View File

@ -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"}

View File

@ -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

View File

@ -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"}

View File

@ -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"}

View File

@ -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({

View File

@ -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"}

View File

@ -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;

View File

@ -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"}

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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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"}

View File

@ -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

View File

@ -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<{

View File

@ -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

View File

@ -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

View File

@ -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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"}

View File

@ -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

View File

@ -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;
}>;

View File

@ -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"}

View File

@ -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

View File

@ -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

View File

@ -1,5 +1,5 @@
{
"name": "prisma-client-652f85dbf9d7be282ff4b16714e4689fe4701aade21c76f6bcc5db624157e639",
"name": "prisma-client-c6d54e22fa4d6137f643638da5d523e99ce84f9544cc793fd89163f1612953c6",
"main": "index.js",
"types": "index.d.ts",
"browser": "index-browser.js",

View File

@ -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])
}

View File

@ -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'
};
/**

View File

@ -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;

View File

@ -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`);

View File

@ -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])
}

View File

@ -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!');
}

View File

@ -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;">&#9679;</span> Anrede, Vorname, Nachname</div>
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">&#9679;</span> Geburtsdatum, Geburtsort</div>
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">&#9679;</span> Ausweisdaten</div>
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">&#9679;</span> Bankdaten (IBAN, BIC)</div>
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">&#9679;</span> E-Mail-Adresse</div>
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">&#9679;</span> Anschrift / Lieferanschriften</div>
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">&#9679;</span> Telefonnummer(n)</div>
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">&#9679;</span> Vertragsdaten / Produkte</div>
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">&#9679;</span> Zählernummern (Strom, Gas)</div>
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">&#9679;</span> Fahrzeugschein- / Führerscheindaten</div>
<div style="display: flex; align-items: baseline; gap: 8px; padding: 5px 0;"><span style="color: #3b82f6;">&#9679;</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());

View File

@ -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' });
}
}

View File

@ -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({

View File

@ -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' });
}
}

View File

@ -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,

View File

@ -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> {

View File

@ -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' });
}
}

View File

@ -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 &amp; 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' });
}
}

View File

@ -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) => {

134
backend/src/lib/prisma.ts Normal file
View File

@ -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;

View File

@ -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();
}

View File

@ -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();
});
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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> {

View File

@ -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,
});
}

View File

@ -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,

View File

@ -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,
},
});
}
}

View File

@ -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(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&nbsp;/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();
});
}

View File

@ -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',
},
};

View File

@ -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,
};
});
}

View File

@ -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 };
}

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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,

View File

@ -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

977
frontend/dist/assets/index-BLUL0czD.js vendored Normal file

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

View File

@ -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>

1
frontend/node_modules/.bin/markdown-it generated vendored Symbolic link
View File

@ -0,0 +1 @@
../markdown-it/bin/markdown-it.mjs

View File

@ -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",

View File

@ -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

View File

@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

1948
frontend/node_modules/.vite/deps/@tiptap_react.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

3464
frontend/node_modules/.vite/deps/@tiptap_starter-kit.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -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"
},

1611
frontend/node_modules/.vite/deps/chunk-MX7X7RGK.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

18528
frontend/node_modules/.vite/deps/chunk-YRIELJS7.js generated vendored Normal file

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